@modesto_ma/dsh-channels 0.3.0 → 0.5.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 +123 -53
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +310 -0
- package/docs/feasibility-and-design.md +1 -1
- package/docs/feishu-onboarding.md +127 -21
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +2 -2
- package/docs/workspace-isolation-design.md +13 -11
- package/lib/client.js +179 -288
- package/lib/client.js.map +1 -1
- package/lib/index.js +12125 -6382
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +12 -12
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +115 -0
- package/src/cards/streaming.ts +56 -7
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +0 -23
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/styles.ts +2 -0
- package/src/commands/registry.ts +2 -2
- package/src/config.ts +190 -32
- package/src/domain.ts +28 -18
- package/src/feishu/agent-context.ts +65 -19
- package/src/feishu/inbound-attachments.ts +440 -3
- package/src/feishu/manifest.ts +247 -0
- package/src/feishu/message-read.ts +1330 -0
- package/src/feishu/native-attachments.ts +998 -0
- package/src/feishu/scopes.ts +171 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +2305 -0
- package/src/feishu-channel.ts +322 -6
- package/src/host/agent.ts +55 -0
- package/src/host/attachments.ts +210 -0
- package/src/host/index.ts +1 -0
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +620 -49
- package/src/service.ts +808 -375
- package/src/session/manager.ts +67 -13
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +90 -89
- package/src/settings/overlay.ts +124 -13
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +156 -82
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +39 -26
- package/src/cleanup.ts +0 -535
- package/src/client/panel/cleanup-card.tsx +0 -106
package/src/session/manager.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { appendSessionHistory, sessionDirOf } from '../domain.ts'
|
|
|
29
29
|
import type { RouteRecord } from '../domain.ts'
|
|
30
30
|
import { mountChannelGuidance } from '../feishu/agent-context.ts'
|
|
31
31
|
import { attachmentsDir } from '../feishu/inbound-attachments.ts'
|
|
32
|
+
import { effectiveAttachmentMode, resolveAttachmentService } from '../feishu/native-attachments.ts'
|
|
32
33
|
import { installRequestPolicy } from './request-policy.ts'
|
|
33
34
|
import type { RequestPolicyController } from './request-policy.ts'
|
|
34
35
|
import { placementServices, restoreTopicDir } from './isolation.ts'
|
|
@@ -65,6 +66,14 @@ export interface SessionManagerDeps {
|
|
|
65
66
|
started(): boolean
|
|
66
67
|
/** Drop streamer state for a session whose owner handle is being released. */
|
|
67
68
|
detachStream(sessionId: string): void
|
|
69
|
+
/**
|
|
70
|
+
* Register this channel's model tools inside one Agent scope. Called from
|
|
71
|
+
* the agent-preset `setup` after the preset is mounted (tools live inside
|
|
72
|
+
* presets in web-profile deployments), and once more for a BORROWED live
|
|
73
|
+
* agent as a best-effort race guard (D7). Optional: unit tests that do not
|
|
74
|
+
* compose model tools omit it.
|
|
75
|
+
*/
|
|
76
|
+
mountTools?(agentCtx: AgentContext, sessionId: string): Promise<void> | void
|
|
68
77
|
warn(message: string, ...args: unknown[]): void
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -82,6 +91,8 @@ export class SessionManager {
|
|
|
82
91
|
private readonly live = new Map<string, LiveAgent>()
|
|
83
92
|
private readonly pendingResume = new Map<string, Promise<LiveAgent>>()
|
|
84
93
|
private readonly policies = new WeakMap<AgentContext, RequestPolicyController>()
|
|
94
|
+
/** Agent scopes this plugin already attempted to mount model tools into. */
|
|
95
|
+
private readonly toolMounts = new WeakSet<AgentContext>()
|
|
85
96
|
|
|
86
97
|
constructor(private readonly deps: SessionManagerDeps) {}
|
|
87
98
|
|
|
@@ -116,7 +127,7 @@ export class SessionManager {
|
|
|
116
127
|
cwd: string,
|
|
117
128
|
agentOptions: Record<string, unknown> = this.agentOptions(),
|
|
118
129
|
): Promise<AgentHandle> {
|
|
119
|
-
const composition = await this.presetComposition(undefined, true)
|
|
130
|
+
const composition = await this.presetComposition(undefined, true, sessionId)
|
|
120
131
|
const handle = await this.deps.ctx.agents.create({
|
|
121
132
|
sessionId,
|
|
122
133
|
meta: {
|
|
@@ -132,15 +143,31 @@ export class SessionManager {
|
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
/**
|
|
135
|
-
* Register the attachment
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
146
|
+
* Register the attachment guidance for one Session. Called after the
|
|
147
|
+
* effective working directory is known (a worktree checkout path is only
|
|
148
|
+
* returned once the worktree exists), so the archive guidance can name the
|
|
149
|
+
* absolute directory. The guidance flavor follows the *effective* attachment
|
|
150
|
+
* backend: an explicitly configured archive deployment, or a native
|
|
151
|
+
* deployment whose host does not mount `ctx.attachments`, keeps the legacy
|
|
152
|
+
* workspace-directory instructions. Best-effort: a missing system-prompt
|
|
153
|
+
* service (headless) is a no-op and a late registration never blocks the turn.
|
|
140
154
|
*/
|
|
141
155
|
async mountGuidance(agent: Agent, directory: string): Promise<void> {
|
|
156
|
+
// With inbound attachments disabled the Agent has no attachment workflow to
|
|
157
|
+
// discover (and the legacy guidance would wrongly claim files are archived).
|
|
158
|
+
if (!this.deps.config.acceptAttachments) return
|
|
159
|
+
const mode = effectiveAttachmentMode(
|
|
160
|
+
this.deps.config.attachmentStore,
|
|
161
|
+
resolveAttachmentService(this.deps.ctx),
|
|
162
|
+
this.deps.config.feishuTools.attachmentFetch,
|
|
163
|
+
this.deps.config.acceptAttachments,
|
|
164
|
+
)
|
|
142
165
|
try {
|
|
143
|
-
await mountChannelGuidance(
|
|
166
|
+
await mountChannelGuidance(
|
|
167
|
+
agent.ctx,
|
|
168
|
+
mode,
|
|
169
|
+
mode === 'archive' ? attachmentsDir(directory) : undefined,
|
|
170
|
+
)
|
|
144
171
|
} catch (error) {
|
|
145
172
|
this.deps.warn(
|
|
146
173
|
'dsh-channels: mounting attachment guidance for session %s failed: %s',
|
|
@@ -166,7 +193,7 @@ export class SessionManager {
|
|
|
166
193
|
if (own !== undefined) return own
|
|
167
194
|
|
|
168
195
|
const foreign = this.deps.ctx.agents.get(sessionId as SessionId)
|
|
169
|
-
if (foreign !== undefined) return this.borrow(sessionId, foreign)
|
|
196
|
+
if (foreign !== undefined) return await this.borrow(sessionId, foreign)
|
|
170
197
|
|
|
171
198
|
let pending = this.pendingResume.get(sessionId)
|
|
172
199
|
if (pending === undefined) {
|
|
@@ -179,9 +206,14 @@ export class SessionManager {
|
|
|
179
206
|
}
|
|
180
207
|
|
|
181
208
|
/** Borrow one foreign live agent and mount this channel's scoped policy. */
|
|
182
|
-
private borrow(sessionId: string, agent: Agent): LiveAgent {
|
|
209
|
+
private async borrow(sessionId: string, agent: Agent): Promise<LiveAgent> {
|
|
183
210
|
const borrowed = borrowLiveAgent(agent, this.mountRequestPolicy(agent.ctx, agent.session))
|
|
184
211
|
this.live.set(sessionId, borrowed)
|
|
212
|
+
// Best-effort tool backfill (D7): the foreign owner may have composed the
|
|
213
|
+
// session without this plugin's setup (web UI resolver, previous plugin
|
|
214
|
+
// instance). mountToolsFor swallows failures, so the borrow itself never
|
|
215
|
+
// fails; awaiting it makes the tool available before the first turn.
|
|
216
|
+
await this.mountToolsFor(agent.ctx, sessionId)
|
|
185
217
|
return borrowed
|
|
186
218
|
}
|
|
187
219
|
|
|
@@ -193,7 +225,7 @@ export class SessionManager {
|
|
|
193
225
|
// (config override or roster default). A session persisted before the
|
|
194
226
|
// preset fix has no preset recorded on its header; re-resolving here is
|
|
195
227
|
// what repairs it on the first resume after a restart.
|
|
196
|
-
const composition = await this.presetComposition(undefined, true)
|
|
228
|
+
const composition = await this.presetComposition(undefined, true, sessionId)
|
|
197
229
|
const handle = await this.deps.ctx.agents.resume({
|
|
198
230
|
resumeSessionId: sessionId,
|
|
199
231
|
agentOptions: this.agentOptions(),
|
|
@@ -215,7 +247,7 @@ export class SessionManager {
|
|
|
215
247
|
// Lost a cold-resume race: another owner published the session while our
|
|
216
248
|
// resume was in flight. Borrow the winner instead of failing the turn.
|
|
217
249
|
const winner = this.deps.ctx.agents.get(sessionId)
|
|
218
|
-
if (winner !== undefined) return this.borrow(sessionId, winner)
|
|
250
|
+
if (winner !== undefined) return await this.borrow(sessionId, winner)
|
|
219
251
|
throw error
|
|
220
252
|
}
|
|
221
253
|
}
|
|
@@ -255,13 +287,13 @@ export class SessionManager {
|
|
|
255
287
|
async renew(record: RouteRecord, oldLive: LiveAgent): Promise<RenewCommit> {
|
|
256
288
|
const sessionId = randomUUID() as SessionId
|
|
257
289
|
const preset = this.effectivePreset(oldLive.agent.session)
|
|
258
|
-
const composition = await this.presetComposition(preset, false)
|
|
290
|
+
const composition = await this.presetComposition(preset, false, sessionId)
|
|
259
291
|
const services: PlacementServices = placementServices(this.deps.ctx)
|
|
260
292
|
const switchable = services.workingDirectory !== undefined
|
|
261
293
|
const workspacePath = record.workspacePath
|
|
262
294
|
const workspace = await this.deps.ctx.workspaceRegistry.create(
|
|
263
295
|
workspacePath,
|
|
264
|
-
workspaceTitleFor(record.chatType, record.chatId, record.
|
|
296
|
+
workspaceTitleFor(record.chatType, record.chatId, record.senderUserId),
|
|
265
297
|
)
|
|
266
298
|
|
|
267
299
|
let newHandle: AgentHandle | undefined
|
|
@@ -358,6 +390,7 @@ export class SessionManager {
|
|
|
358
390
|
private async presetComposition(
|
|
359
391
|
requestedPreset: string | undefined = this.deps.config.agentPreset,
|
|
360
392
|
useRosterDefault = true,
|
|
393
|
+
sessionId?: string,
|
|
361
394
|
): Promise<PresetComposition> {
|
|
362
395
|
// Read through ctx.get (safe lookup): only the web profile ships a roster,
|
|
363
396
|
// and direct property access on a cordis context throws for services the
|
|
@@ -369,6 +402,7 @@ export class SessionManager {
|
|
|
369
402
|
// cannot guarantee that the explicit channel value wins.
|
|
370
403
|
this.mountRequestPolicy(agentCtx)
|
|
371
404
|
await mountChannelCommands(agentCtx)
|
|
405
|
+
if (sessionId !== undefined) await this.mountToolsFor(agentCtx, sessionId)
|
|
372
406
|
}
|
|
373
407
|
if (presets === undefined || (requestedPreset === undefined && !useRosterDefault)) {
|
|
374
408
|
return { setup: channelCommandsOnly }
|
|
@@ -380,10 +414,30 @@ export class SessionManager {
|
|
|
380
414
|
this.mountRequestPolicy(agentCtx)
|
|
381
415
|
await presets.mount(agentCtx, resolved.id)
|
|
382
416
|
await mountChannelCommands(agentCtx)
|
|
417
|
+
if (sessionId !== undefined) await this.mountToolsFor(agentCtx, sessionId)
|
|
383
418
|
},
|
|
384
419
|
}
|
|
385
420
|
}
|
|
386
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Mount the channel model tools into one Agent scope, at most once per
|
|
424
|
+
* scope. A failure is logged and swallowed: missing tools never fail a
|
|
425
|
+
* session (attachment fetch degrades to its structured failures).
|
|
426
|
+
*/
|
|
427
|
+
private async mountToolsFor(agentCtx: AgentContext, sessionId: string): Promise<void> {
|
|
428
|
+
if (this.deps.mountTools === undefined) return
|
|
429
|
+
if (this.toolMounts.has(agentCtx)) return
|
|
430
|
+
try {
|
|
431
|
+
await this.deps.mountTools(agentCtx, sessionId)
|
|
432
|
+
this.toolMounts.add(agentCtx)
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.deps.warn(
|
|
435
|
+
'dsh-channels: mounting channel tools for session %s failed: %s',
|
|
436
|
+
sessionId, String(error),
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
387
441
|
/** Effective preset after folding any durable runtime preset switch. */
|
|
388
442
|
private effectivePreset(session: Session): string | undefined {
|
|
389
443
|
let selected = session.header.agentPreset
|
package/src/session/routes.ts
CHANGED
|
@@ -135,8 +135,7 @@ export class RouteStore {
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Retire one active thread route into a persistent tombstone regardless of
|
|
138
|
-
* the configured TTL.
|
|
139
|
-
* close topics earlier than the route TTL.
|
|
138
|
+
* the configured TTL.
|
|
140
139
|
*/
|
|
141
140
|
async retire(key: string, now = Date.now()): Promise<ExpiredRouteRecord | undefined> {
|
|
142
141
|
const record = this.get(key)
|
|
@@ -145,7 +144,7 @@ export class RouteStore {
|
|
|
145
144
|
key,
|
|
146
145
|
chatId: record.chatId,
|
|
147
146
|
chatType: record.chatType,
|
|
148
|
-
|
|
147
|
+
senderUserId: record.senderUserId,
|
|
149
148
|
expiredAt: now,
|
|
150
149
|
workspacePath: record.workspacePath,
|
|
151
150
|
...(record.sessionDir === undefined ? {} : { sessionDir: record.sessionDir }),
|
package/src/settings/contract.ts
CHANGED
|
@@ -20,13 +20,11 @@ export const CREDENTIALS_ROUTE = '/dsh-channels/credentials'
|
|
|
20
20
|
export const RECONNECT_ROUTE = '/dsh-channels/reconnect'
|
|
21
21
|
/** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
|
|
22
22
|
export const CATALOG_ROUTE = '/dsh-channels/catalog'
|
|
23
|
-
/** 手动执行一次过期目录清理(POST,无请求体)。 */
|
|
24
|
-
export const CLEANUP_ROUTE = '/dsh-channels/cleanup'
|
|
25
23
|
/** 访问与授权只读快照(GET/HEAD):准入模式、管理员、群/用户白名单、待处理申请。 */
|
|
26
24
|
export const ACCESS_ROUTE = '/dsh-channels/access'
|
|
27
25
|
/** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
|
|
28
26
|
export const ACCESS_GROUPS_ROUTE = '/dsh-channels/access/groups'
|
|
29
|
-
/** 用户白名单增删(POST/DELETE,body `{
|
|
27
|
+
/** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
|
|
30
28
|
export const ACCESS_USERS_ROUTE = '/dsh-channels/access/users'
|
|
31
29
|
/** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
|
|
32
30
|
export const ACCESS_REQUESTS_ROUTE = '/dsh-channels/access/requests'
|
|
@@ -40,9 +38,10 @@ export const ACCESS_REQUESTS_ROUTE = '/dsh-channels/access/requests'
|
|
|
40
38
|
export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
|
|
41
39
|
|
|
42
40
|
/**
|
|
43
|
-
* 可由 Web
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* 可由 Web 面板覆盖的配置项。顶层键对应 `src/config.ts` 的字段;`feishuTools.*`
|
|
42
|
+
* 是嵌套字段(覆盖层里仍写成 `{ "feishuTools": { ... } }` 对象)。覆盖值写进
|
|
43
|
+
* `$DSH_HOME/dsh-channels/settings.json`,优先级高于 profile 的
|
|
44
|
+
* `cordis.patch.yml`(见 README「Web 设置面板」)。
|
|
46
45
|
*
|
|
47
46
|
* 刻意不包含 `appId` / `appSecret`:它们是**凭据引用名**,面板改为直接
|
|
48
47
|
* 编辑引用背后的值(`PUT /dsh-channels/credentials`,经 DSH 凭据服务写入),
|
|
@@ -55,20 +54,34 @@ export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
|
|
|
55
54
|
export const EDITABLE_KEYS = [
|
|
56
55
|
// 工作区
|
|
57
56
|
'baseDir',
|
|
58
|
-
'workspaceRetention',
|
|
59
|
-
'workspaceRetentionAuto',
|
|
60
57
|
// 行为
|
|
61
58
|
'dmMode',
|
|
62
59
|
'requireMention',
|
|
63
60
|
'threadFallback',
|
|
64
61
|
'welcomeOnboarding',
|
|
62
|
+
'slashCommandPanel',
|
|
65
63
|
'allowPermissionSwitch',
|
|
66
64
|
// 准入与授权(管理员/准入模式/申请有效期;白名单由「访问与授权」卡片管理)
|
|
67
|
-
'
|
|
65
|
+
'adminUserId',
|
|
68
66
|
'accessMode',
|
|
69
67
|
'authRequestTtlDays',
|
|
70
|
-
//
|
|
68
|
+
// 内容与附件(v2 管线;`feishuTools.*` 是嵌套配置,覆盖层仍写成嵌套对象)
|
|
71
69
|
'acceptAttachments',
|
|
70
|
+
'attachmentStore',
|
|
71
|
+
'prefetchImages',
|
|
72
|
+
'prefetchTimeoutMs',
|
|
73
|
+
'prefetchImageMaxBytes',
|
|
74
|
+
'prefetchTotalMaxBytes',
|
|
75
|
+
'historyBootstrapLimit',
|
|
76
|
+
// 模型工具(feishuTools 子项)
|
|
77
|
+
'feishuTools.attachmentFetch',
|
|
78
|
+
'feishuTools.attachmentFetchMaxResources',
|
|
79
|
+
'feishuTools.attachmentFetchMaxBytes',
|
|
80
|
+
'feishuTools.attachmentFetchTimeoutMs',
|
|
81
|
+
'feishuTools.topicMessages',
|
|
82
|
+
'feishuTools.topicMessagesTimeoutMs',
|
|
83
|
+
'feishuTools.sendFile',
|
|
84
|
+
'promptContextTimeoutMs',
|
|
72
85
|
// 模型
|
|
73
86
|
'provider',
|
|
74
87
|
'model',
|
|
@@ -89,6 +102,7 @@ export type EditableKey = (typeof EDITABLE_KEYS)[number]
|
|
|
89
102
|
export const RESTART_KEYS = [
|
|
90
103
|
'dmMode',
|
|
91
104
|
'requireMention',
|
|
105
|
+
'slashCommandPanel',
|
|
92
106
|
'refreshMs',
|
|
93
107
|
'statusRefreshMs',
|
|
94
108
|
'loggerLevel',
|
|
@@ -104,7 +118,7 @@ export type FieldKind = 'boolean' | 'number' | 'text' | 'select'
|
|
|
104
118
|
* panel tab (see {@link TAB_FIELD_GROUPS}); the Host is the source of truth for
|
|
105
119
|
* which key belongs to which group.
|
|
106
120
|
*/
|
|
107
|
-
export type FieldGroup = 'workspace' | 'behavior' | 'content' | 'model' | 'streaming' | 'access'
|
|
121
|
+
export type FieldGroup = 'workspace' | 'behavior' | 'content' | 'tools' | 'model' | 'streaming' | 'access'
|
|
108
122
|
|
|
109
123
|
/** One selectable value rendered by the generic form. */
|
|
110
124
|
export interface FieldOptionWire {
|
|
@@ -249,8 +263,40 @@ export interface ChannelStatusWire {
|
|
|
249
263
|
overlayPath: string
|
|
250
264
|
/** 正在连接或重连时为 true,此时面板的写操作按钮禁用。 */
|
|
251
265
|
busy: boolean
|
|
266
|
+
/** 连接后的权限自检结果;未连接或自检尚未完成时缺省。 */
|
|
267
|
+
scopeCheck?: ScopeCheckWire
|
|
268
|
+
/** 连接后飞书原生斜杠指令面板的同步结果;未连接或同步尚未完成时缺省。 */
|
|
269
|
+
slashCommandSync?: SlashCommandSyncWire
|
|
252
270
|
}
|
|
253
271
|
|
|
272
|
+
/** One plugin need whose scopes are all missing. */
|
|
273
|
+
export interface MissingScopeWire {
|
|
274
|
+
/** Scope names, preferred first; granting any one satisfies the need. */
|
|
275
|
+
scopes: string[]
|
|
276
|
+
/** `required` breaks a core path; `optional` only degrades a feature. */
|
|
277
|
+
level: 'required' | 'optional'
|
|
278
|
+
/** What the plugin loses without it. */
|
|
279
|
+
purpose: string
|
|
280
|
+
/** Developer-console permission page filtered to the preferred scope. */
|
|
281
|
+
consoleUrl: string
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Post-connect scope self-check outcome. */
|
|
285
|
+
export type ScopeCheckWire =
|
|
286
|
+
/** Every applicable need is granted. */
|
|
287
|
+
| { state: 'ok'; checkedAt: number }
|
|
288
|
+
/** Some needs are missing; `consoleUrl` opens the console filtered to all of them. */
|
|
289
|
+
| { state: 'missing'; checkedAt: number; missing: MissingScopeWire[]; consoleUrl: string }
|
|
290
|
+
/** The grant list could not be read; `error` carries the Feishu reason. */
|
|
291
|
+
| { state: 'error'; checkedAt: number; error: string }
|
|
292
|
+
|
|
293
|
+
/** Post-connect native slash-command panel sync outcome. */
|
|
294
|
+
export type SlashCommandSyncWire =
|
|
295
|
+
/** The panel lists exactly the registry commands; counts are this sync's writes. */
|
|
296
|
+
| { state: 'ok'; checkedAt: number; created: number; updated: number; removed: number; total: number }
|
|
297
|
+
/** A Feishu call failed; `error` carries the Feishu reason. */
|
|
298
|
+
| { state: 'error'; checkedAt: number; error: string }
|
|
299
|
+
|
|
254
300
|
/** The whole editable configuration surface, with provenance. */
|
|
255
301
|
export interface PanelConfigWire {
|
|
256
302
|
/** profile `cordis.patch.yml` 提供的值(覆盖层之下)。 */
|
|
@@ -281,9 +327,9 @@ export interface AccessGroupWire {
|
|
|
281
327
|
addedAt: number
|
|
282
328
|
}
|
|
283
329
|
|
|
284
|
-
/** One authorized user `
|
|
330
|
+
/** One authorized user `user_id` (global across every authorized group). */
|
|
285
331
|
export interface AccessUserWire {
|
|
286
|
-
|
|
332
|
+
userId: string
|
|
287
333
|
/** Display name captured at authorization time. */
|
|
288
334
|
name?: string
|
|
289
335
|
/** Which authorization entry produced the grant. */
|
|
@@ -297,7 +343,7 @@ export interface AccessRequestWire {
|
|
|
297
343
|
/** Opaque token; never derived from the target id. */
|
|
298
344
|
token: string
|
|
299
345
|
kind: 'group' | 'user'
|
|
300
|
-
/** `chat_id` for a group request, `
|
|
346
|
+
/** `chat_id` for a group request, `user_id` for a user request. */
|
|
301
347
|
targetId: string
|
|
302
348
|
/** Display name of the requested target (group name / user name), when known. */
|
|
303
349
|
name?: string
|
|
@@ -320,9 +366,14 @@ export interface AccessRequestWire {
|
|
|
320
366
|
export interface AccessWire {
|
|
321
367
|
mode: 'open' | 'guarded'
|
|
322
368
|
admin: {
|
|
323
|
-
|
|
369
|
+
/** The effective administrator: configured `adminUserId`, else the app owner. */
|
|
370
|
+
userId?: string
|
|
371
|
+
/** Where `userId` came from: explicit config or the app-owner fallback. */
|
|
372
|
+
source?: 'config' | 'owner'
|
|
324
373
|
/** false while guarded mode would refuse every conversation (fail closed). */
|
|
325
374
|
configured: boolean
|
|
375
|
+
/** Why the app-owner fallback is unavailable, when its lookup failed. */
|
|
376
|
+
ownerLookupError?: string
|
|
326
377
|
}
|
|
327
378
|
/**
|
|
328
379
|
* Whether the persisted whitelists were loaded. While the channel holds no
|
|
@@ -344,51 +395,10 @@ export type AccessMutationResponse =
|
|
|
344
395
|
export interface PanelWire {
|
|
345
396
|
status: ChannelStatusWire
|
|
346
397
|
config: PanelConfigWire
|
|
347
|
-
cleanup: CleanupPreviewWire
|
|
348
398
|
/** 访问与授权卡片的数据(准入模式、管理员、白名单、待处理申请)。 */
|
|
349
399
|
access: AccessWire
|
|
350
400
|
}
|
|
351
401
|
|
|
352
|
-
/**
|
|
353
|
-
* What the workspace-retention cleanup would delete right now. Counts are
|
|
354
|
-
* zero and `error` is set when the Host cannot compute the preview (for
|
|
355
|
-
* example the cleanup seam is unavailable); the panel stays usable.
|
|
356
|
-
*/
|
|
357
|
-
export interface CleanupPreviewWire {
|
|
358
|
-
/** Effective `workspaceRetention` value (`off` / `7d` / `30d` / `90d` / `180d`). */
|
|
359
|
-
retention: string
|
|
360
|
-
/** Retention threshold in milliseconds; absent when `retention` is `off`. */
|
|
361
|
-
thresholdMs?: number
|
|
362
|
-
/** Periodic cleanup runs at startup and once per day. */
|
|
363
|
-
auto: boolean
|
|
364
|
-
/** Plugin-created workspaces under the current baseDir (user projects). */
|
|
365
|
-
workspaces: number
|
|
366
|
-
/** Directories older than the threshold (legacy topic workspaces + per-topic dirs). */
|
|
367
|
-
candidates: number
|
|
368
|
-
/** Total bytes currently occupied by the candidates. */
|
|
369
|
-
candidateBytes: number
|
|
370
|
-
/** Why the preview could not be computed (counts are zero then). */
|
|
371
|
-
error?: string
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Outcome of one manual cleanup run (partial failures do not abort the run). */
|
|
375
|
-
export interface CleanupRunWire {
|
|
376
|
-
ok: boolean
|
|
377
|
-
deletedWorkspaces: number
|
|
378
|
-
/** Per-topic working directories removed (current layout). */
|
|
379
|
-
deletedSessionDirs: number
|
|
380
|
-
retiredRoutes: number
|
|
381
|
-
freedBytes: number
|
|
382
|
-
/** One `path: message` line per directory that could not be deleted. */
|
|
383
|
-
failures: string[]
|
|
384
|
-
error?: string
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** `POST /dsh-channels/cleanup` response. */
|
|
388
|
-
export type CleanupResponse =
|
|
389
|
-
| { ok: true; panel: PanelWire; result: CleanupRunWire }
|
|
390
|
-
| { ok: false; error: string }
|
|
391
|
-
|
|
392
402
|
/**
|
|
393
403
|
* `POST /dsh-channels/pick-directory` response. `canceled` is a normal
|
|
394
404
|
* outcome (the operator dismissed the OS dialog), not an error; `path` is an
|
|
@@ -486,7 +496,7 @@ export type PanelTabId = (typeof PANEL_TABS)[number]['id']
|
|
|
486
496
|
/** Which editable field groups each tab renders (the tabs own one or two). */
|
|
487
497
|
export const TAB_FIELD_GROUPS: Readonly<Record<PanelTabId, readonly FieldGroup[]>> = {
|
|
488
498
|
connection: [],
|
|
489
|
-
sessions: ['behavior', 'content'],
|
|
499
|
+
sessions: ['behavior', 'content', 'tools'],
|
|
490
500
|
model: ['model'],
|
|
491
501
|
workspace: ['workspace'],
|
|
492
502
|
access: ['access'],
|
|
@@ -498,6 +508,7 @@ export const FIELD_GROUP_LABELS: Readonly<Record<FieldGroup, string>> = {
|
|
|
498
508
|
workspace: '工作区与文件',
|
|
499
509
|
behavior: '行为',
|
|
500
510
|
content: '内容与附件',
|
|
511
|
+
tools: '模型工具',
|
|
501
512
|
model: '模型与推理',
|
|
502
513
|
streaming: '流式卡片与日志',
|
|
503
514
|
access: '准入与授权',
|
|
@@ -522,6 +533,21 @@ export const PANEL_COPY = {
|
|
|
522
533
|
statusSummaryConnecting: '正在连接飞书长连接…',
|
|
523
534
|
statusSummaryError: '最近一次连接飞书失败,请检查凭据与网络后重试。',
|
|
524
535
|
lastError: '最近一次连接失败',
|
|
536
|
+
scopeCheckTitle: '权限自检',
|
|
537
|
+
scopeCheckOk: '已开通插件需要的全部飞书权限。',
|
|
538
|
+
scopeCheckMissing: '以下飞书权限未开通,开通后需在开发者后台发布新版本:',
|
|
539
|
+
scopeCheckOpenAll: '在开发者后台开通全部缺失权限',
|
|
540
|
+
scopeCheckOpenOne: '去开通',
|
|
541
|
+
scopeCheckOptional: '可选',
|
|
542
|
+
scopeCheckRequired: '必需',
|
|
543
|
+
scopeCheckError: '无法读取应用的权限授予状态:',
|
|
544
|
+
slashCommandSyncTitle: '斜杠指令面板',
|
|
545
|
+
slashCommandSyncOk: '插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。',
|
|
546
|
+
slashCommandSyncTotal: '命令数',
|
|
547
|
+
slashCommandSyncCreated: '本次新增',
|
|
548
|
+
slashCommandSyncUpdated: '本次更新',
|
|
549
|
+
slashCommandSyncRemoved: '本次删除',
|
|
550
|
+
slashCommandSyncError: '同步飞书原生斜杠指令面板失败(不影响直接输入命令):',
|
|
525
551
|
missingConfig: '缺少配置',
|
|
526
552
|
statusHintUnconfigured: '配置完成后插件会自动连接;也可以稍后在本页手动连接。',
|
|
527
553
|
connectionInfo: '连接信息',
|
|
@@ -570,9 +596,9 @@ export const PANEL_COPY = {
|
|
|
570
596
|
credentialsPendingHint: '填写后点「保存凭据」;两项齐全时插件会自动连接飞书。',
|
|
571
597
|
// ---- tabs / forms -------------------------------------------------------
|
|
572
598
|
connectionTabHelp: '连接状态与飞书应用凭据。凭据不完整时不会连接飞书。',
|
|
573
|
-
sessionsTabHelp: '
|
|
599
|
+
sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
|
|
574
600
|
modelTabHelp: '飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。',
|
|
575
|
-
workspaceTabHelp: '
|
|
601
|
+
workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。',
|
|
576
602
|
accessTabHelp: '准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。',
|
|
577
603
|
advancedTabHelp: '流式刷新等低层参数、Web 覆盖文件与只读信息。',
|
|
578
604
|
save: '保存更改',
|
|
@@ -619,37 +645,12 @@ export const PANEL_COPY = {
|
|
|
619
645
|
advancedHelp: '这些值来自 profile 配置,不能在面板里编辑。',
|
|
620
646
|
readOnlyCard: '部署级参数',
|
|
621
647
|
// ---- model --------------------------------------------------------------
|
|
622
|
-
// ---- cleanup ------------------------------------------------------------
|
|
623
|
-
cleanupCard: '目录清理',
|
|
624
|
-
cleanupCardHelp: '删除超过保留期且不再活跃的飞书话题目录(以及旧版本按话题建立的工作区目录);用户工作区本身、DSH 会话日志都不会删除。删除不可恢复:相关旧话题会失效并提示已过期。',
|
|
625
|
-
cleanupRetention: '保留期',
|
|
626
|
-
cleanupAutoLabel: '自动清理',
|
|
627
|
-
cleanupCandidates: '可清理目录',
|
|
628
|
-
cleanupWorkspaces: '插件工作区',
|
|
629
|
-
cleanupCandidateBytes: '预计释放空间',
|
|
630
|
-
cleanupRetentionOff: '关闭(不删除)',
|
|
631
|
-
cleanupAutoOn: '已开启(启动时 + 每天一次)',
|
|
632
|
-
cleanupAutoOff: '已关闭(仅手动执行)',
|
|
633
|
-
cleanupAutoInactive: '未执行(保留期为关闭)',
|
|
634
|
-
cleanupNow: '立即清理',
|
|
635
|
-
cleanupRunning: '清理中…',
|
|
636
|
-
cleanupConfirm: '确定清理过期目录吗?该操作不可恢复,会删除这些目录内的全部文件(包括 attachments/ 附件)。',
|
|
637
|
-
cleanupConfirmHint: '删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。',
|
|
638
|
-
cleanupDisabledOff: '保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。',
|
|
639
|
-
cleanupDisabledEmpty: '没有超过保留期的过期目录。',
|
|
640
|
-
cleanupPreviewFailed: '无法读取清理预览',
|
|
641
|
-
cleanupDone: '清理完成',
|
|
642
|
-
cleanupFailed: '清理失败',
|
|
643
|
-
cleanupDeleted: '已删除工作区',
|
|
644
|
-
cleanupDeletedDirs: '话题目录',
|
|
645
|
-
cleanupRetired: '已清理话题路由',
|
|
646
|
-
cleanupFreed: '释放空间',
|
|
647
|
-
cleanupFailures: '以下目录未能删除:',
|
|
648
648
|
// ---- access -------------------------------------------------------------
|
|
649
649
|
accessCard: '访问与授权',
|
|
650
|
-
accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的
|
|
650
|
+
accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。',
|
|
651
651
|
accessModeOpenWarning: '当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。',
|
|
652
|
-
accessAdminMissingWarning: '当前准入=guarded
|
|
652
|
+
accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。',
|
|
653
|
+
accessAdminOwnerNotice: '未配置管理员,已自动使用应用所有者作为管理员:',
|
|
653
654
|
accessGroupsTitle: '已授权群',
|
|
654
655
|
accessGroupsEmpty: '尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。',
|
|
655
656
|
accessUsersTitle: '已授权用户',
|
|
@@ -658,7 +659,7 @@ export const PANEL_COPY = {
|
|
|
658
659
|
accessRequestsEmpty: '没有待处理的授权申请。',
|
|
659
660
|
accessGroupChatIdPlaceholder: '群 chat_id(oc_…)',
|
|
660
661
|
accessGroupNamePlaceholder: '群名(可选)',
|
|
661
|
-
|
|
662
|
+
accessUserIdPlaceholder: '用户 user_id',
|
|
662
663
|
accessUserNamePlaceholder: '昵称(可选)',
|
|
663
664
|
accessAddGroup: '添加群',
|
|
664
665
|
accessAddUser: '添加用户',
|
|
@@ -680,5 +681,5 @@ export const PANEL_COPY = {
|
|
|
680
681
|
accessRequestAllowed: '已授权',
|
|
681
682
|
accessRequestDenied: '已忽略',
|
|
682
683
|
accessHints: '三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。',
|
|
683
|
-
accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;
|
|
684
|
+
accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。',
|
|
684
685
|
} as const
|