@modesto_ma/dsh-channels 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/lib/client.js +53 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +445 -665
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/analyze/contract.ts +7 -3
- package/src/analyze/launcher.ts +28 -5
- package/src/analyze/server.ts +60 -54
- package/src/client/api.ts +28 -30
- package/src/client/catalog.ts +1 -1
- package/src/client/panel/advanced-tab.tsx +1 -1
- package/src/config.ts +4 -13
- package/src/health.ts +0 -12
- package/src/host/agent.ts +24 -1
- package/src/index.ts +6 -4
- package/src/service.ts +18 -38
- package/src/session/manager.ts +4 -4
- package/src/session/request-policy.ts +64 -5
- package/src/settings/contract.ts +49 -34
- package/src/settings/routes.ts +159 -443
- package/src/settings/surface.ts +5 -6
- package/src/streamer.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modesto_ma/dsh-channels",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "DeepSeek Harness (DSH) 飞书渠道插件:一个 dsh 实例对应一个飞书机器人,私聊/群聊/话题群 @ 触发会话,话题绑定 session,卡片 2.0 流式回复;Web 左侧栏提供「飞书渠道」状态与常用配置面板",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
91
91
|
"@types/node": "^26.6.5",
|
|
92
92
|
"@types/react": "~18.3.31",
|
|
93
|
-
"conventional-changelog-conventionalcommits": "
|
|
93
|
+
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
94
94
|
"react": "18.3.1",
|
|
95
95
|
"semantic-release": "^25.0.9",
|
|
96
96
|
"tsdown": "0.22.2",
|
package/src/analyze/contract.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wire contract of the log-platform analyze API.
|
|
3
3
|
*
|
|
4
|
-
* `POST /dsh-channels
|
|
4
|
+
* `POST /api/dsh-channels.logalyze` on the DSH Web server, with
|
|
5
5
|
* `Authorization: Bearer <token>`, an `Idempotency-Key` header, and a JSON
|
|
6
6
|
* body naming the requester, the trigger time, the subject (application +
|
|
7
7
|
* error code), the log-platform detail pages, the target group, and the
|
|
@@ -17,8 +17,12 @@ import { createHash } from 'node:crypto'
|
|
|
17
17
|
import { z } from 'zod'
|
|
18
18
|
import { FEISHU_USER_ID_PATTERN } from '../domain.ts'
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Exact DSH Web route of the analyze API. It follows the host's plain-HTTP
|
|
22
|
+
* `/api/<name>.<method>` naming; an exact route wins over the `/api` RPC prefix
|
|
23
|
+
* route, so the request carries this API's bearer token, not a DSH login.
|
|
24
|
+
*/
|
|
25
|
+
export const ANALYZE_ROUTE = '/api/dsh-channels.logalyze'
|
|
22
26
|
|
|
23
27
|
/** Largest accepted request body (the prompt dominates it). */
|
|
24
28
|
export const ANALYZE_MAX_BODY_BYTES = 1024 * 1024
|
package/src/analyze/launcher.ts
CHANGED
|
@@ -85,9 +85,28 @@ export class AnalyzeLauncher implements AnalyzeHandler {
|
|
|
85
85
|
private readonly accepting = new Map<string, PendingAccept>()
|
|
86
86
|
/** Background analyses still starting (drained on shutdown). */
|
|
87
87
|
private readonly running = new Set<Promise<void>>()
|
|
88
|
+
/** Whether requests are admitted: from {@link open} after start until {@link close}. */
|
|
89
|
+
private admitting = false
|
|
88
90
|
|
|
89
91
|
constructor(private readonly deps: AnalyzeLauncherDeps) {}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Start admitting requests. Called at channel start after
|
|
95
|
+
* {@link markInterrupted}, so a new request is never marked interrupted.
|
|
96
|
+
*/
|
|
97
|
+
open(): void {
|
|
98
|
+
this.admitting = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Stop admitting requests (503) ahead of shutdown. */
|
|
102
|
+
close(): void {
|
|
103
|
+
this.admitting = false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
enabled(): boolean {
|
|
107
|
+
return this.deps.config.analyzeEnabled
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
async token(): Promise<string | undefined> {
|
|
92
111
|
return await this.deps.resolveToken()
|
|
93
112
|
}
|
|
@@ -109,17 +128,21 @@ export class AnalyzeLauncher implements AnalyzeHandler {
|
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
/**
|
|
112
|
-
* Wait for every
|
|
113
|
-
* `started` turns false, so each one ends on
|
|
114
|
-
* first prompt was already sent, as an
|
|
131
|
+
* Wait for every acceptance in flight, then every background analysis that
|
|
132
|
+
* is still starting. Called after `started` turns false, so each one ends on
|
|
133
|
+
* its failure card or, when its first prompt was already sent, as an
|
|
134
|
+
* ordinary topic.
|
|
115
135
|
*/
|
|
116
136
|
async drain(): Promise<void> {
|
|
137
|
+
// Acceptances still sending the root card or writing the record finish
|
|
138
|
+
// first; each one adds its launch to `running` before it settles.
|
|
139
|
+
await Promise.allSettled([...this.accepting.values()].map(pending => pending.response))
|
|
117
140
|
await Promise.allSettled([...this.running])
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
/**
|
|
121
144
|
* Mark every analysis still `running` as interrupted, on its card and in
|
|
122
|
-
* its record. Called at channel start before
|
|
145
|
+
* its record. Called at channel start before admission opens, so each
|
|
123
146
|
* such record belongs to a previous instance whose first turn never
|
|
124
147
|
* settled. A card that cannot be updated (deleted, recalled) is logged and
|
|
125
148
|
* still recorded as interrupted, so it is not retried on every start.
|
|
@@ -141,7 +164,7 @@ export class AnalyzeLauncher implements AnalyzeHandler {
|
|
|
141
164
|
private async accept(request: AnalyzeRequest, key: string, bodyHash: string): Promise<AnalyzeResponse> {
|
|
142
165
|
const channel = this.deps.channel()
|
|
143
166
|
const domain = this.deps.domain()
|
|
144
|
-
if (!this.deps.started() || channel === undefined || domain === undefined) {
|
|
167
|
+
if (!this.admitting || !this.deps.started() || channel === undefined || domain === undefined) {
|
|
145
168
|
return refused(503, '飞书渠道尚未连接,请稍后重试')
|
|
146
169
|
}
|
|
147
170
|
const stored = domain.analyzeRequests.get(key)
|
package/src/analyze/server.ts
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The analyze API's
|
|
2
|
+
* The analyze API's HTTP route on the DSH Web server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Registers the exact route `POST /api/dsh-channels.logalyze` (see
|
|
5
|
+
* `./contract.ts`) on the host `webServer`; hosts without one (headless) have
|
|
6
|
+
* no analyze API. This module owns the transport-level checks, in order:
|
|
7
|
+
* path, the `analyzeEnabled` switch, method, bearer token, `Idempotency-Key`,
|
|
8
|
+
* media type, body size, and JSON validation; everything after validation
|
|
9
|
+
* belongs to the {@link AnalyzeHandler}. Bodies, tokens, and prompts are never
|
|
10
|
+
* logged.
|
|
9
11
|
*
|
|
10
12
|
* @module dsh-channels/analyze/server
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
import type { AddressInfo } from 'node:net'
|
|
16
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
17
|
+
import type { HostContext } from '../dsh-types.ts'
|
|
17
18
|
import {
|
|
18
19
|
ANALYZE_MAX_BODY_BYTES, ANALYZE_ROUTE, IDEMPOTENCY_KEY_PATTERN, parseAnalyzeRequest,
|
|
19
20
|
} from './contract.ts'
|
|
20
21
|
import type { AnalyzeRequest, AnalyzeResponse } from './contract.ts'
|
|
21
22
|
|
|
22
|
-
/** The
|
|
23
|
+
/** The narrow `ctx.webServer` surface the analyze route registers into. */
|
|
24
|
+
export interface HostWebServer {
|
|
25
|
+
register(route: {
|
|
26
|
+
kind: 'exact'
|
|
27
|
+
path: string
|
|
28
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
29
|
+
}): () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The business side of the route. */
|
|
23
33
|
export interface AnalyzeHandler {
|
|
34
|
+
/**
|
|
35
|
+
* Whether the API is switched on (`analyzeEnabled`), read per request.
|
|
36
|
+
* @returns false to answer every request with 404.
|
|
37
|
+
*/
|
|
38
|
+
enabled(): boolean
|
|
24
39
|
/**
|
|
25
40
|
* Resolve the expected bearer token for this request.
|
|
26
41
|
* @returns the token, or undefined when it is not configured.
|
|
@@ -35,14 +50,6 @@ export interface AnalyzeHandler {
|
|
|
35
50
|
analyze(request: AnalyzeRequest, idempotencyKey: string): Promise<AnalyzeResponse>
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
/** A bound analyze listener. */
|
|
39
|
-
export interface AnalyzeServer {
|
|
40
|
-
/** The bound address (the real port when `0` was requested). */
|
|
41
|
-
readonly address: AddressInfo
|
|
42
|
-
/** Stop accepting connections and close idle ones. */
|
|
43
|
-
close(): Promise<void>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
53
|
class HttpError extends Error {
|
|
47
54
|
constructor(readonly status: number, message: string) {
|
|
48
55
|
super(message)
|
|
@@ -112,6 +119,10 @@ export async function handleAnalyzeHttp(
|
|
|
112
119
|
refuse(res, 404, '未知路径')
|
|
113
120
|
return
|
|
114
121
|
}
|
|
122
|
+
if (!handler.enabled()) {
|
|
123
|
+
refuse(res, 404, '日志分析接口未开启,请在设置面板「高级 → 日志分析接口」打开开关')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
115
126
|
if ((req.method ?? 'GET').toUpperCase() !== 'POST') {
|
|
116
127
|
refuse(res, 405, '只接受 POST', { allow: 'POST' })
|
|
117
128
|
return
|
|
@@ -155,47 +166,42 @@ export async function handleAnalyzeHttp(
|
|
|
155
166
|
send(res, await handler.analyze(parsed.value, idempotencyKey))
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
/** Split `host:port` (`[v6]:port` allowed) into listen arguments. */
|
|
159
|
-
export function parseListenAddress(listen: string): { host: string; port: number } {
|
|
160
|
-
const separator = listen.lastIndexOf(':')
|
|
161
|
-
const host = listen.slice(0, separator).replace(/^\[(.*)\]$/u, '$1')
|
|
162
|
-
return { host, port: Number(listen.slice(separator + 1)) }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
169
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
170
|
+
* Register the analyze route on the host Web server once it is available.
|
|
171
|
+
* Requests skip the DSH login gate on purpose: the bearer token is this API's
|
|
172
|
+
* only credential.
|
|
173
|
+
* @param ctx - the plugin context.
|
|
168
174
|
* @param handler - the business side.
|
|
169
175
|
* @param warn - diagnostics sink for unexpected handler failures.
|
|
170
|
-
* @returns the bound server; rejects when the address cannot be bound.
|
|
171
176
|
*/
|
|
172
|
-
export
|
|
173
|
-
|
|
177
|
+
export function mountAnalyzeRoute(
|
|
178
|
+
ctx: HostContext,
|
|
174
179
|
handler: AnalyzeHandler,
|
|
175
180
|
warn: (message: string, ...args: unknown[]) => void,
|
|
176
|
-
):
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
181
|
+
): void {
|
|
182
|
+
const attach = (webCtx: HostContext): void => {
|
|
183
|
+
const server = webCtx.get?.('webServer') as HostWebServer | undefined
|
|
184
|
+
if (server === undefined) return
|
|
185
|
+
webCtx.effect(() => server.register({
|
|
186
|
+
kind: 'exact',
|
|
187
|
+
path: ANALYZE_ROUTE,
|
|
188
|
+
handler: async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
await handleAnalyzeHttp(req, res, handler)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
warn('dsh-channels: analyze request failed: %s', String(error))
|
|
193
|
+
if (!res.headersSent) refuse(res, 500, `分析请求处理失败:${String(error)}`)
|
|
194
|
+
else res.destroy()
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
}), 'dsh-channels: analyze route')
|
|
198
|
+
}
|
|
199
|
+
const inject = (ctx as {
|
|
200
|
+
inject?: (dependencies: string[], callback: (child: HostContext) => void) => unknown
|
|
201
|
+
}).inject
|
|
202
|
+
if (typeof inject === 'function') {
|
|
203
|
+
inject.call(ctx, ['webServer'], attach)
|
|
204
|
+
return
|
|
200
205
|
}
|
|
206
|
+
attach(ctx)
|
|
201
207
|
}
|
package/src/client/api.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Same-origin HTTP client for the settings panel. No typert contract: the
|
|
3
|
-
* Host half registers
|
|
3
|
+
* Host half registers exact DSH `/api` Fetch routes and this module fetches them.
|
|
4
4
|
*
|
|
5
5
|
* @module dsh-channels/client/api
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
ACCESS_GROUPS_ROUTE,
|
|
10
|
-
ACCESS_REQUESTS_ROUTE,
|
|
11
9
|
ACCESS_ROUTE,
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
ADD_ACCESS_GROUP_ROUTE,
|
|
11
|
+
ADD_ACCESS_USER_ROUTE,
|
|
12
|
+
ADD_WIKI_USER_ROUTE,
|
|
14
13
|
CATALOG_ROUTE,
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
CLEAR_CONFIG_ROUTE,
|
|
15
|
+
CLEAR_CREDENTIAL_ROUTE,
|
|
17
16
|
PANEL_ROUTE,
|
|
18
17
|
PICK_DIRECTORY_ROUTE,
|
|
19
18
|
PROFILES_MAX_IDS,
|
|
20
19
|
PROFILES_ROUTE,
|
|
21
20
|
RECONNECT_ROUTE,
|
|
21
|
+
REMOVE_ACCESS_GROUP_ROUTE,
|
|
22
|
+
REMOVE_ACCESS_USER_ROUTE,
|
|
23
|
+
REMOVE_WIKI_USER_ROUTE,
|
|
24
|
+
RESOLVE_ACCESS_REQUEST_ROUTE,
|
|
25
|
+
SAVE_CONFIG_ROUTE,
|
|
26
|
+
SAVE_CREDENTIALS_ROUTE,
|
|
22
27
|
SESSION_ORIGIN_ROUTE,
|
|
23
28
|
STATUS_ROUTE,
|
|
24
29
|
} from '../settings/contract.ts'
|
|
@@ -138,8 +143,8 @@ export async function fetchCatalog(provider?: string, model?: string): Promise<C
|
|
|
138
143
|
|
|
139
144
|
/** Persist a partial edit. Validation failures come back as `{ok:false}`. */
|
|
140
145
|
export async function saveConfig(values: EditableValues): Promise<SaveConfigResponse> {
|
|
141
|
-
const response = await request(
|
|
142
|
-
method: '
|
|
146
|
+
const response = await request(SAVE_CONFIG_ROUTE, {
|
|
147
|
+
method: 'POST',
|
|
143
148
|
headers: { 'content-type': 'application/json' },
|
|
144
149
|
body: JSON.stringify({ values }),
|
|
145
150
|
})
|
|
@@ -158,7 +163,7 @@ export async function saveConfig(values: EditableValues): Promise<SaveConfigResp
|
|
|
158
163
|
|
|
159
164
|
/** Drop every Web override and return the refreshed panel. */
|
|
160
165
|
export async function clearConfig(): Promise<ClearConfigResponse> {
|
|
161
|
-
const response = await request(
|
|
166
|
+
const response = await request(CLEAR_CONFIG_ROUTE, { method: 'POST' })
|
|
162
167
|
const text = await response.text()
|
|
163
168
|
if (response.status === 400) {
|
|
164
169
|
try {
|
|
@@ -205,8 +210,8 @@ export async function pickDirectory(): Promise<PickDirectoryResponse> {
|
|
|
205
210
|
|
|
206
211
|
/** Store provided credential values (the request/response never carry stored values). */
|
|
207
212
|
export async function saveCredentials(values: SaveCredentialsRequest): Promise<SaveCredentialsResponse> {
|
|
208
|
-
const response = await request(
|
|
209
|
-
method: '
|
|
213
|
+
const response = await request(SAVE_CREDENTIALS_ROUTE, {
|
|
214
|
+
method: 'POST',
|
|
210
215
|
headers: { 'content-type': 'application/json' },
|
|
211
216
|
body: JSON.stringify(values),
|
|
212
217
|
})
|
|
@@ -224,8 +229,8 @@ export async function saveCredentials(values: SaveCredentialsRequest): Promise<S
|
|
|
224
229
|
|
|
225
230
|
/** Remove the stored value behind one credential reference. */
|
|
226
231
|
export async function clearCredential(field: CredentialField): Promise<ClearCredentialResponse> {
|
|
227
|
-
const response = await request(
|
|
228
|
-
method: '
|
|
232
|
+
const response = await request(CLEAR_CREDENTIAL_ROUTE, {
|
|
233
|
+
method: 'POST',
|
|
229
234
|
headers: { 'content-type': 'application/json' },
|
|
230
235
|
body: JSON.stringify({ field }),
|
|
231
236
|
})
|
|
@@ -247,13 +252,9 @@ export async function fetchAccess(): Promise<AccessWire> {
|
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
/** One access write: a 400 body already is the wire union; others throw. */
|
|
250
|
-
async function mutateAccess(
|
|
251
|
-
path: string,
|
|
252
|
-
method: 'POST' | 'DELETE',
|
|
253
|
-
body: unknown,
|
|
254
|
-
): Promise<AccessMutationResponse> {
|
|
255
|
+
async function mutateAccess(path: string, body: unknown): Promise<AccessMutationResponse> {
|
|
255
256
|
const response = await request(path, {
|
|
256
|
-
method,
|
|
257
|
+
method: 'POST',
|
|
257
258
|
headers: { 'content-type': 'application/json' },
|
|
258
259
|
body: JSON.stringify(body),
|
|
259
260
|
})
|
|
@@ -272,43 +273,40 @@ async function mutateAccess(
|
|
|
272
273
|
/** Authorize one group; resolves with the refreshed access snapshot. */
|
|
273
274
|
export async function addAccessGroup(chatId: string, name?: string): Promise<AccessMutationResponse> {
|
|
274
275
|
return mutateAccess(
|
|
275
|
-
|
|
276
|
-
'POST',
|
|
276
|
+
ADD_ACCESS_GROUP_ROUTE,
|
|
277
277
|
name === undefined || name === '' ? { chatId } : { chatId, name },
|
|
278
278
|
)
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/** Revoke one group authorization (takes effect on the next message). */
|
|
282
282
|
export async function removeAccessGroup(chatId: string): Promise<AccessMutationResponse> {
|
|
283
|
-
return mutateAccess(
|
|
283
|
+
return mutateAccess(REMOVE_ACCESS_GROUP_ROUTE, { chatId })
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/** Authorize one user `user_id` (global across authorized groups). */
|
|
287
287
|
export async function addAccessUser(userId: string, name?: string): Promise<AccessMutationResponse> {
|
|
288
288
|
return mutateAccess(
|
|
289
|
-
|
|
290
|
-
'POST',
|
|
289
|
+
ADD_ACCESS_USER_ROUTE,
|
|
291
290
|
name === undefined || name === '' ? { userId } : { userId, name },
|
|
292
291
|
)
|
|
293
292
|
}
|
|
294
293
|
|
|
295
294
|
/** Revoke one user authorization (takes effect on the next message). */
|
|
296
295
|
export async function removeAccessUser(userId: string): Promise<AccessMutationResponse> {
|
|
297
|
-
return mutateAccess(
|
|
296
|
+
return mutateAccess(REMOVE_ACCESS_USER_ROUTE, { userId })
|
|
298
297
|
}
|
|
299
298
|
|
|
300
299
|
/** Add one user `user_id` to the wiki whitelist. */
|
|
301
300
|
export async function addWikiUser(userId: string, name?: string): Promise<AccessMutationResponse> {
|
|
302
301
|
return mutateAccess(
|
|
303
|
-
|
|
304
|
-
'POST',
|
|
302
|
+
ADD_WIKI_USER_ROUTE,
|
|
305
303
|
name === undefined || name === '' ? { userId } : { userId, name },
|
|
306
304
|
)
|
|
307
305
|
}
|
|
308
306
|
|
|
309
307
|
/** Remove one user from the wiki whitelist (takes effect on the next wiki tool call). */
|
|
310
308
|
export async function removeWikiUser(userId: string): Promise<AccessMutationResponse> {
|
|
311
|
-
return mutateAccess(
|
|
309
|
+
return mutateAccess(REMOVE_WIKI_USER_ROUTE, { userId })
|
|
312
310
|
}
|
|
313
311
|
|
|
314
312
|
/** Settle one pending authorization request (same resolver as the Feishu card). */
|
|
@@ -316,5 +314,5 @@ export async function resolveAccessRequest(
|
|
|
316
314
|
token: string,
|
|
317
315
|
decision: 'allow' | 'deny',
|
|
318
316
|
): Promise<AccessMutationResponse> {
|
|
319
|
-
return mutateAccess(
|
|
317
|
+
return mutateAccess(RESOLVE_ACCESS_REQUEST_ROUTE, { token, decision })
|
|
320
318
|
}
|
package/src/client/catalog.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -83,9 +83,6 @@ export const DEFAULT_PROMPT_CONTEXT_TIMEOUT_MS = 3_000
|
|
|
83
83
|
/** Conventional credential reference holding the log-platform analyze API bearer token. */
|
|
84
84
|
export const DEFAULT_ANALYZE_TOKEN_REF = 'DSH_CHANNELS_ANALYZE_TOKEN'
|
|
85
85
|
|
|
86
|
-
/** `host:port` listen address of the analyze API (`[v6]:port` for IPv6 literals). */
|
|
87
|
-
export const ANALYZE_LISTEN_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+):(?:[1-9][0-9]{0,4})$/u
|
|
88
|
-
|
|
89
86
|
/** A credential reference: `FEISHU_APP_ID`-style name resolved through ctx.credentials. */
|
|
90
87
|
const credentialRefSchema = z.string().regex(
|
|
91
88
|
CREDENTIAL_REF_PATTERN,
|
|
@@ -425,17 +422,11 @@ export const configSchema = z.object({
|
|
|
425
422
|
*/
|
|
426
423
|
questionTimeoutMs: z.number().int().min(10_000).max(3_600_000).default(DEFAULT_QUESTION_TIMEOUT_MS),
|
|
427
424
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* (default) keeps the API off. The listener runs while the Feishu connection
|
|
432
|
-
* is up and is rebound on reconnect.
|
|
425
|
+
* Switch for the log-platform analyze API (`POST /api/dsh-channels.logalyze`
|
|
426
|
+
* on the DSH Web server). Off (default) answers 404; read per request, so
|
|
427
|
+
* toggling it needs no reconnect. Needs a host with a Web server.
|
|
433
428
|
*/
|
|
434
|
-
|
|
435
|
-
ANALYZE_LISTEN_PATTERN,
|
|
436
|
-
'must be host:port, e.g. 0.0.0.0:3090 or 127.0.0.1:3090',
|
|
437
|
-
).refine(value => Number(value.slice(value.lastIndexOf(':') + 1)) <= 65_535, 'port must be 1-65535')
|
|
438
|
-
.optional(),
|
|
429
|
+
analyzeEnabled: z.boolean().default(false),
|
|
439
430
|
/**
|
|
440
431
|
* Credential reference holding the analyze API bearer token (defaults to
|
|
441
432
|
* `DSH_CHANNELS_ANALYZE_TOKEN`). Resolved per request, so rotating the value
|
package/src/health.ts
CHANGED
|
@@ -55,8 +55,6 @@ export interface HealthSnapshotInput {
|
|
|
55
55
|
routeCount: number
|
|
56
56
|
liveAgentCount: number
|
|
57
57
|
inflightTurnCount: number
|
|
58
|
-
/** Why the configured analyze listener could not bind; absent when it is bound or not configured. */
|
|
59
|
-
analyzeListenError?: string
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
export interface HealthSnapshot extends Omit<HealthSnapshotInput, 'fibers' | 'botOpenId'> {
|
|
@@ -150,7 +148,6 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
150
148
|
|| fibers.pending > 0
|
|
151
149
|
|| fibers.loading > 0
|
|
152
150
|
|| fibers.unloading > 0
|
|
153
|
-
|| input.analyzeListenError !== undefined
|
|
154
151
|
) {
|
|
155
152
|
level = 'degraded'
|
|
156
153
|
} else {
|
|
@@ -179,10 +176,6 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
179
176
|
)
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
if (input.analyzeListenError !== undefined) {
|
|
183
|
-
reasons.push(`日志分析接口监听失败:${input.analyzeListenError}`)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
179
|
return {
|
|
187
180
|
level,
|
|
188
181
|
fibers,
|
|
@@ -193,7 +186,6 @@ export function foldHealthSnapshot(input: HealthSnapshotInput): HealthSnapshot {
|
|
|
193
186
|
routeCount: input.routeCount,
|
|
194
187
|
liveAgentCount: input.liveAgentCount,
|
|
195
188
|
inflightTurnCount: input.inflightTurnCount,
|
|
196
|
-
...(input.analyzeListenError === undefined ? {} : { analyzeListenError: input.analyzeListenError }),
|
|
197
189
|
inactiveFibers: inactive,
|
|
198
190
|
inactiveTotal,
|
|
199
191
|
reasons,
|
|
@@ -237,9 +229,5 @@ export function buildHealthCard(snapshot: HealthSnapshot): object {
|
|
|
237
229
|
tag: 'markdown',
|
|
238
230
|
content: `**dsh-channels**\n${snapshot.started ? 'started' : 'stopped'} · 持久化路由 ${snapshot.routeCount} · live Agent ${snapshot.liveAgentCount} · in-flight turn ${snapshot.inflightTurnCount}`,
|
|
239
231
|
},
|
|
240
|
-
...(snapshot.analyzeListenError === undefined ? [] : [{
|
|
241
|
-
tag: 'markdown',
|
|
242
|
-
content: `**日志分析接口**\n❌ 监听失败:${snapshot.analyzeListenError}`,
|
|
243
|
-
}]),
|
|
244
232
|
])
|
|
245
233
|
}
|
package/src/host/agent.ts
CHANGED
|
@@ -35,10 +35,31 @@ export interface AgentRequestConfig {
|
|
|
35
35
|
|
|
36
36
|
/** Agent-scoped request override installed before the first prompt assembly. */
|
|
37
37
|
export type AgentRequestListener = (
|
|
38
|
-
payload: { turn: number; step: number; signal: AbortSignal },
|
|
38
|
+
payload: { turn: number; step: number; signal: AbortSignal; agent?: Agent },
|
|
39
39
|
next: () => Promise<AgentRequestConfig>,
|
|
40
40
|
) => Promise<AgentRequestConfig>
|
|
41
41
|
|
|
42
|
+
/** Complete provider, model, and optional reasoning effort for one Session's model steps. */
|
|
43
|
+
export interface ModelSelection {
|
|
44
|
+
readonly provider: string
|
|
45
|
+
readonly model: string
|
|
46
|
+
/** Absent means the adapter/provider default effort. */
|
|
47
|
+
readonly reasoningEffort?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Agent-owned durable model choice (`agent.model`, DSH 0.2.2-alpha.1 and
|
|
52
|
+
* later). On these hosts an `agent/request` listener that changes provider,
|
|
53
|
+
* model, or reasoning effort fails the step, so a switch must go through
|
|
54
|
+
* `select()` before the next step starts.
|
|
55
|
+
*/
|
|
56
|
+
export interface AgentModel {
|
|
57
|
+
/** Choice for the next step: recorded selection, then last request, then live options. */
|
|
58
|
+
current(): Readonly<ModelSelection> | undefined
|
|
59
|
+
/** Record the next step's complete selection (`model/selection`); an equal recorded choice is a no-op. */
|
|
60
|
+
select(selection: Readonly<ModelSelection>): void
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/** Cordis listener placement used to make an explicit channel override outermost. */
|
|
43
64
|
export interface AgentRequestListenerOptions {
|
|
44
65
|
prepend?: boolean
|
|
@@ -133,6 +154,8 @@ export interface AgentContext {
|
|
|
133
154
|
export interface Agent {
|
|
134
155
|
readonly id: SessionId
|
|
135
156
|
readonly options: AgentOptions
|
|
157
|
+
/** Durable model choice; absent on DSH 0.2.1 hosts, which route through `agent/request`. */
|
|
158
|
+
readonly model?: AgentModel
|
|
136
159
|
readonly session: Session
|
|
137
160
|
readonly status: 'idle' | 'running'
|
|
138
161
|
readonly inbox: {
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { defaultChannelFactory } from './feishu-channel.ts'
|
|
|
23
23
|
import type { ChannelFactory } from './feishu-channel.ts'
|
|
24
24
|
import { OverlayStore, defaultOverlayPath } from './settings/overlay.ts'
|
|
25
25
|
import { mountSettingsRoutes } from './settings/routes.ts'
|
|
26
|
+
import { mountAnalyzeRoute } from './analyze/server.ts'
|
|
26
27
|
import { SettingsSurface } from './settings/surface.ts'
|
|
27
28
|
import { createHostCatalog } from './settings/catalog.ts'
|
|
28
29
|
import { createSettingsDirectoryPicker } from './settings/directory-picker.ts'
|
|
@@ -41,7 +42,7 @@ export type * from './questions.ts'
|
|
|
41
42
|
export type * from './settings/contract.ts'
|
|
42
43
|
export * from './access/index.ts'
|
|
43
44
|
export type { AccessGroupRecord, AccessRequestRecord, AccessUserRecord } from './domain.ts'
|
|
44
|
-
export { CATALOG_ROUTE,
|
|
45
|
+
export { CATALOG_ROUTE, CLEAR_CONFIG_ROUTE, CLEAR_CREDENTIAL_ROUTE, EDITABLE_KEYS, PANEL_COPY, PANEL_ROUTE, PICK_DIRECTORY_ROUTE, RECONNECT_ROUTE, RESTART_KEYS, SAVE_CONFIG_ROUTE, SAVE_CREDENTIALS_ROUTE, STATUS_ROUTE } from './settings/contract.ts'
|
|
45
46
|
export type { PickDirectoryResponse } from './settings/contract.ts'
|
|
46
47
|
export { defaultOverlayPath, mergeOverlay, normalizeOverlay, OverlayStore, parseOverlay, pickEditable, resolveDshHome } from './settings/overlay.ts'
|
|
47
48
|
export { applyCatalog, emptyCatalog, FIELD_DESCRIPTORS, normalizeCredentialValue, SettingsSurface, withDirectoryBrowse } from './settings/surface.ts'
|
|
@@ -50,8 +51,8 @@ export { createHostCatalog } from './settings/catalog.ts'
|
|
|
50
51
|
export type { CatalogWarn, SettingsCatalog } from './settings/catalog.ts'
|
|
51
52
|
export { createSettingsDirectoryPicker } from './settings/directory-picker.ts'
|
|
52
53
|
export type { HostDirectoryPickerService, SettingsDirectoryPicker } from './settings/directory-picker.ts'
|
|
53
|
-
export {
|
|
54
|
-
export type {
|
|
54
|
+
export { mountSettingsRoutes, registerSettingsRoutes } from './settings/routes.ts'
|
|
55
|
+
export type { HostConnection, HostFetchRoute } from './settings/routes.ts'
|
|
55
56
|
export type { ApprovalOutcome, ApprovalRequest, ApprovalAnswererListener, AssistantStreamFrame, AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionItem, AskUserQuestionRequestEvent, UserQuestionAnswererListener, AgentCancelCause, SessionStatsView, TokenUsageView, ContextPressureView, SessionProjections } from './dsh-types.ts'
|
|
56
57
|
export { buildCard, buildInterruptionCard, cardJsonBytes, CARD_MAX_BYTES, CARD_TARGET_BYTES, REASONING_MAX_BYTES, INTERRUPTION_NOTICE, MAX_TABLES_PER_CARD, PANEL_TITLES, PANEL_CHEVRON_TOKEN, segmentCardContent, splitBodyForCard, stripTables, tableBlocks, TRUNCATION_SUFFIX, buildApprovalCard, buildApprovalSettledCard, buildQuestionCard, buildQuestionSettledCard, MAX_QUESTION_OPTIONS } from './cards.ts'
|
|
57
58
|
export { ApprovalAnswerer } from './approvals.ts'
|
|
@@ -112,7 +113,7 @@ export function apply(ctx: HostContext, config?: unknown): void {
|
|
|
112
113
|
{ warn: (message, ...args) => ctx.logger?.warn?.(message, ...args) },
|
|
113
114
|
)
|
|
114
115
|
const service = new FeishuService(ctx, resolved, factory, undefined, overlay)
|
|
115
|
-
// Settings routes ride
|
|
116
|
+
// Settings routes ride DSH's authenticated `/api` channel when this deployment has one.
|
|
116
117
|
// The catalog resolves the panel's model selectors from live host services;
|
|
117
118
|
// the optional native directory picker backs the workspace-root「选择目录…」
|
|
118
119
|
// button (absent in headless/remote compositions → plain text input).
|
|
@@ -124,6 +125,7 @@ export function apply(ctx: HostContext, config?: unknown): void {
|
|
|
124
125
|
createHostCatalog(ctx, warn),
|
|
125
126
|
createSettingsDirectoryPicker(ctx),
|
|
126
127
|
))
|
|
128
|
+
mountAnalyzeRoute(ctx, service.analyzeHandler(), warn)
|
|
127
129
|
ctx.effect(() => {
|
|
128
130
|
// Boot loads the Web overlay and connects only when the Feishu application
|
|
129
131
|
// credentials are complete; an incomplete configuration or a failed
|