@modesto_ma/dsh-guanxiang 0.2.0-beta.1 → 0.2.0-beta.2
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 +65 -1
- package/lib/client.js +242 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +442 -34
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/analyze/contract.ts +124 -0
- package/src/analyze/server.ts +195 -0
- package/src/analyze/topics.ts +109 -0
- package/src/client/page.tsx +89 -5
- package/src/config.ts +14 -0
- package/src/index.ts +10 -4
- package/src/service.ts +47 -12
- package/src/settings/contract.ts +36 -2
package/src/index.ts
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* 改进时的 `guanxiang_propose_skill` 工具(经用户授权后把子 Agent 的 SKILL
|
|
9
9
|
* 修改提议推送到新分支并创建合并请求)。应用仓库在设置
|
|
10
10
|
* 面板中只填 git 地址,由插件拉取;应用描述来自各仓库的
|
|
11
|
-
* `.dsh/guanxiang.yaml
|
|
11
|
+
* `.dsh/guanxiang.yaml`。另提供日志平台分析接口 `POST /open-api/logalyze`,
|
|
12
|
+
* 经 dsh-channels 的 `feishuTopics` 服务在飞书群话题中开始分析。
|
|
12
13
|
*
|
|
13
14
|
* @module dsh-guanxiang
|
|
14
15
|
*/
|
|
15
16
|
|
|
17
|
+
import { mountAnalyzeRoute } from './analyze/server.ts'
|
|
16
18
|
import { resolveConfig } from './config.ts'
|
|
17
19
|
import type { HostContext } from './host.ts'
|
|
18
20
|
import { GuanxiangService } from './service.ts'
|
|
@@ -32,7 +34,9 @@ export { ENTRY_SKILL_NAME, renderEntrySkill } from './skill/entry.ts'
|
|
|
32
34
|
export { RepoRegistry } from './repos/registry.ts'
|
|
33
35
|
export { readAppManifest, MANIFEST_PATH } from './repos/manifest.ts'
|
|
34
36
|
export { GuanxiangService } from './service.ts'
|
|
35
|
-
export { SETTINGS_ROUTE, STATE_ROUTE, SYNC_ROUTE } from './settings/contract.ts'
|
|
37
|
+
export { ANALYZE_ROUTE, SETTINGS_ROUTE, STATE_ROUTE, SYNC_ROUTE } from './settings/contract.ts'
|
|
38
|
+
export { analyzeRequestHash, parseAnalyzeRequest } from './analyze/contract.ts'
|
|
39
|
+
export type { AnalyzeAccepted, AnalyzeRefused, AnalyzeRequest } from './analyze/contract.ts'
|
|
36
40
|
|
|
37
41
|
/** Cordis plugin name. */
|
|
38
42
|
export const name = 'guanxiang'
|
|
@@ -50,8 +54,9 @@ export interface GuanxiangPluginOptions extends ServiceOptions {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
|
-
* Install the entry skill, the dispatch and SKILL proposal tools,
|
|
54
|
-
* clone or update the configured
|
|
57
|
+
* Install the entry skill, the dispatch and SKILL proposal tools, the settings
|
|
58
|
+
* routes, and the analyze route, then clone or update the configured
|
|
59
|
+
* repositories in the background.
|
|
55
60
|
* @param ctx - plugin context.
|
|
56
61
|
* @param config - loader-validated configuration.
|
|
57
62
|
* @param options - test seams.
|
|
@@ -61,6 +66,7 @@ export function apply(ctx: HostContext, config?: unknown, options: GuanxiangPlug
|
|
|
61
66
|
service.registerSkill()
|
|
62
67
|
service.registerTool()
|
|
63
68
|
mountSettingsRoutes(ctx, service)
|
|
69
|
+
mountAnalyzeRoute(ctx, service.analyzeGate())
|
|
64
70
|
ctx.effect(() => {
|
|
65
71
|
void service.boot().catch((error: unknown) => {
|
|
66
72
|
ctx.logger?.error?.('guanxiang: 启动失败:%s', error instanceof Error ? error.message : String(error))
|
package/src/service.ts
CHANGED
|
@@ -9,13 +9,16 @@ import { z } from 'zod'
|
|
|
9
9
|
import { applyOverlay, defaultsOf, describeIssue, editableSettingsSchema, expandHome } from './config.ts'
|
|
10
10
|
import type { GuanxiangConfig, SettingsOverlay } from './config.ts'
|
|
11
11
|
import { abortable } from './abort.ts'
|
|
12
|
+
import type { AnalyzeGate } from './analyze/server.ts'
|
|
13
|
+
import { FEISHU_TOPICS_SERVICE, lookupTopics } from './analyze/topics.ts'
|
|
12
14
|
import { Dispatcher, isDelegatedAgent, isInside } from './dispatch/tool.ts'
|
|
13
15
|
import type { HostApproval, HostContext, HostCredentials, ToolActivationContext, WorkingDirectoryService } from './host.ts'
|
|
14
16
|
import { credentialEnvironment, credentialFor, isAcceptedToken, MAX_TOKEN_LENGTH } from './repos/credentials.ts'
|
|
15
17
|
import { runGit } from './repos/git.ts'
|
|
16
18
|
import type { GitRunner } from './repos/git.ts'
|
|
17
19
|
import { RepoRegistry } from './repos/registry.ts'
|
|
18
|
-
import
|
|
20
|
+
import { ANALYZE_ROUTE } from './settings/contract.ts'
|
|
21
|
+
import type { AnalyzeWire, CredentialsWire, EditableSettings, StateWire, TokenStatusWire } from './settings/contract.ts'
|
|
19
22
|
import { diffOverlay, overriddenKeys } from './settings/overlay.ts'
|
|
20
23
|
import type { OverlayStore } from './settings/overlay.ts'
|
|
21
24
|
import { createEntrySkillProvider } from './skill/entry.ts'
|
|
@@ -47,6 +50,7 @@ export class GuanxiangService {
|
|
|
47
50
|
private readonly skill: ReturnType<typeof createEntrySkillProvider>
|
|
48
51
|
private active = true
|
|
49
52
|
private readonly refreshWaitMs: number
|
|
53
|
+
private readonly analyzeTokenRef: string
|
|
50
54
|
private readonly booted = Promise.withResolvers<void>()
|
|
51
55
|
|
|
52
56
|
/**
|
|
@@ -63,6 +67,7 @@ export class GuanxiangService {
|
|
|
63
67
|
) {
|
|
64
68
|
this.defaults = defaultsOf(config)
|
|
65
69
|
this.refreshWaitMs = config.refreshWaitMs
|
|
70
|
+
this.analyzeTokenRef = config.analyzeTokenRef
|
|
66
71
|
this.current = this.defaults
|
|
67
72
|
const git = options.git ?? runGit
|
|
68
73
|
this.registry = new RepoRegistry({
|
|
@@ -195,6 +200,33 @@ export class GuanxiangService {
|
|
|
195
200
|
...this.overlayError === undefined ? {} : { overlayError: this.overlayError },
|
|
196
201
|
repos: this.registry.status(),
|
|
197
202
|
credentials: await this.credentialsState(),
|
|
203
|
+
analyze: await this.analyzeState(),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @returns the analyze route's switch and bearer token, read per request. */
|
|
208
|
+
analyzeGate(): AnalyzeGate {
|
|
209
|
+
return {
|
|
210
|
+
enabled: () => this.current.analyze.enabled,
|
|
211
|
+
tokenEnv: () => this.analyzeTokenRef,
|
|
212
|
+
token: async () => {
|
|
213
|
+
try {
|
|
214
|
+
return (await this.credentialStore()?.resolve(this.analyzeTokenRef))?.value
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.ctx.logger?.warn?.('guanxiang: 读取分析接口令牌 %s 失败:%s', this.analyzeTokenRef, error instanceof Error ? error.message : String(error))
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async analyzeState(): Promise<AnalyzeWire> {
|
|
224
|
+
const store = this.credentialStore()
|
|
225
|
+
return {
|
|
226
|
+
route: ANALYZE_ROUTE,
|
|
227
|
+
tokenEnv: this.analyzeTokenRef,
|
|
228
|
+
...store === undefined ? {} : { token: await describeToken(store, this.analyzeTokenRef) },
|
|
229
|
+
topics: lookupTopics(this.ctx.get?.(FEISHU_TOPICS_SERVICE)).availability,
|
|
198
230
|
}
|
|
199
231
|
}
|
|
200
232
|
|
|
@@ -206,14 +238,7 @@ export class GuanxiangService {
|
|
|
206
238
|
const store = this.credentialStore()
|
|
207
239
|
if (store === undefined) return { available: false, tokens: [] }
|
|
208
240
|
const names = [...new Set(this.current.gitCredentials.map(credential => credential.tokenEnv))]
|
|
209
|
-
const tokens = await Promise.all(names.map(
|
|
210
|
-
try {
|
|
211
|
-
const info = await store.describe(tokenEnv)
|
|
212
|
-
return { tokenEnv, configured: info.configured, ...info.source === undefined ? {} : { source: info.source }, writable: info.writable }
|
|
213
|
-
} catch (error) {
|
|
214
|
-
return { tokenEnv, configured: false, writable: false, error: error instanceof Error ? error.message : String(error) }
|
|
215
|
-
}
|
|
216
|
-
}))
|
|
241
|
+
const tokens = await Promise.all(names.map(tokenEnv => describeToken(store, tokenEnv)))
|
|
217
242
|
return { available: true, tokens }
|
|
218
243
|
}
|
|
219
244
|
|
|
@@ -237,10 +262,11 @@ export class GuanxiangService {
|
|
|
237
262
|
|
|
238
263
|
/**
|
|
239
264
|
* Store the token of one access-token configuration in DSH's credential
|
|
240
|
-
* store, then re-sync the repositories that use it
|
|
265
|
+
* store, then re-sync the repositories that use it; or store the analyze
|
|
266
|
+
* API's bearer token (`analyzeTokenRef`), which applies from the next request.
|
|
241
267
|
* @param raw - request body `{ tokenEnv, value }`.
|
|
242
268
|
* @returns the new panel state.
|
|
243
|
-
* @throws SettingsError when the body is invalid, the name is
|
|
269
|
+
* @throws SettingsError when the body is invalid, the name is neither referenced by the saved settings nor `analyzeTokenRef`, or no credential store is loaded.
|
|
244
270
|
*/
|
|
245
271
|
async setToken(raw: unknown): Promise<StateWire> {
|
|
246
272
|
const parsed = tokenSetSchema.safeParse(raw)
|
|
@@ -262,7 +288,7 @@ export class GuanxiangService {
|
|
|
262
288
|
}
|
|
263
289
|
|
|
264
290
|
private async writeToken(tokenEnv: string, value: string | undefined): Promise<StateWire> {
|
|
265
|
-
if (!this.current.gitCredentials.some(credential => credential.tokenEnv === tokenEnv)) throw new SettingsError(`${tokenEnv} 不是已保存的访问令牌配置引用的名称`)
|
|
291
|
+
if (tokenEnv !== this.analyzeTokenRef && !this.current.gitCredentials.some(credential => credential.tokenEnv === tokenEnv)) throw new SettingsError(`${tokenEnv} 不是已保存的访问令牌配置引用的名称`)
|
|
266
292
|
const store = this.credentialStore()
|
|
267
293
|
if (store === undefined) throw new SettingsError('当前 DSH 组合没有凭据服务(@deepseek-ai/dsh-credentials-local),无法保存令牌')
|
|
268
294
|
if (value === undefined) await store.unset(tokenEnv)
|
|
@@ -329,3 +355,12 @@ export class GuanxiangService {
|
|
|
329
355
|
this.skill.invalidate()
|
|
330
356
|
}
|
|
331
357
|
}
|
|
358
|
+
|
|
359
|
+
async function describeToken(store: HostCredentials, tokenEnv: string): Promise<TokenStatusWire> {
|
|
360
|
+
try {
|
|
361
|
+
const info = await store.describe(tokenEnv)
|
|
362
|
+
return { tokenEnv, configured: info.configured, ...info.source === undefined ? {} : { source: info.source }, writable: info.writable }
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return { tokenEnv, configured: false, writable: false, error: error instanceof Error ? error.message : String(error) }
|
|
365
|
+
}
|
|
366
|
+
}
|
package/src/settings/contract.ts
CHANGED
|
@@ -11,9 +11,15 @@ export const STATE_ROUTE = '/guanxiang/state'
|
|
|
11
11
|
export const SETTINGS_ROUTE = '/guanxiang/settings'
|
|
12
12
|
/** POST:立即重新拉取全部仓库,或 body `{ url }` 指定的一个仓库。 */
|
|
13
13
|
export const SYNC_ROUTE = '/guanxiang/sync'
|
|
14
|
-
/** PUT:body `{ tokenEnv, value }` 把令牌存入 DSH 凭据库;DELETE:body `{ tokenEnv }`
|
|
14
|
+
/** PUT:body `{ tokenEnv, value }` 把令牌存入 DSH 凭据库;DELETE:body `{ tokenEnv }` 删除已存的值。只接受已保存的访问令牌配置引用的名称和分析接口令牌的名称。 */
|
|
15
15
|
export const TOKEN_ROUTE = '/guanxiang/token'
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* POST:日志平台分析接口(Bearer 令牌鉴权,不走 DSH 登录)。它在 DSH 的
|
|
19
|
+
* `/api` 命名空间之外,网关可以把它和需要登录的 DSH 接口分开暴露。
|
|
20
|
+
*/
|
|
21
|
+
export const ANALYZE_ROUTE = '/open-api/logalyze'
|
|
22
|
+
|
|
17
23
|
/** One configured application repository. */
|
|
18
24
|
export interface RepoSpec {
|
|
19
25
|
/** Git remote address, cloned with the host machine's existing git credentials or a matching {@link GitCredentialSpec}. */
|
|
@@ -57,6 +63,12 @@ export interface SkillUpdateSettings {
|
|
|
57
63
|
maxFileBytes: number
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/** The log-platform analyze API ({@link ANALYZE_ROUTE}). */
|
|
67
|
+
export interface AnalyzeSettings {
|
|
68
|
+
/** Whether the API accepts requests; switched off it answers 404. */
|
|
69
|
+
enabled: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
/** The settings the Web panel can override; every field is required on the wire. */
|
|
61
73
|
export interface EditableSettings {
|
|
62
74
|
/** Absolute directory holding the cloned repositories (`~` expands to the home directory). */
|
|
@@ -75,10 +87,12 @@ export interface EditableSettings {
|
|
|
75
87
|
download: DownloadSettings
|
|
76
88
|
/** SKILL improvement proposals. */
|
|
77
89
|
skillUpdates: SkillUpdateSettings
|
|
90
|
+
/** Log-platform analyze API. */
|
|
91
|
+
analyze: AnalyzeSettings
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
/** Keys of {@link EditableSettings}, in panel order. */
|
|
81
|
-
export const EDITABLE_KEYS = ['reposDir', 'repos', 'gitCredentials', 'fetchIntervalMs', 'maxAppsPerRequest', 'dispatchTimeoutMs', 'download', 'skillUpdates'] as const satisfies readonly (keyof EditableSettings)[]
|
|
95
|
+
export const EDITABLE_KEYS = ['reposDir', 'repos', 'gitCredentials', 'fetchIntervalMs', 'maxAppsPerRequest', 'dispatchTimeoutMs', 'download', 'skillUpdates', 'analyze'] as const satisfies readonly (keyof EditableSettings)[]
|
|
82
96
|
|
|
83
97
|
/** One editable key. */
|
|
84
98
|
export type EditableKey = (typeof EDITABLE_KEYS)[number]
|
|
@@ -146,6 +160,25 @@ export interface CredentialsWire {
|
|
|
146
160
|
tokens: TokenStatusWire[]
|
|
147
161
|
}
|
|
148
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Whether dsh-channels provides the `feishuTopics` service the analyze API
|
|
165
|
+
* posts through: `missing` while it is not loaded (or older than 0.8, which
|
|
166
|
+
* serves the API itself), `incompatible` when its contract version is not
|
|
167
|
+
* the one this plugin speaks.
|
|
168
|
+
*/
|
|
169
|
+
export type TopicsAvailability = 'ready' | 'missing' | 'incompatible'
|
|
170
|
+
|
|
171
|
+
/** Analyze API view of the panel. */
|
|
172
|
+
export interface AnalyzeWire {
|
|
173
|
+
/** Route the log platform posts to. */
|
|
174
|
+
route: string
|
|
175
|
+
/** Credential reference (`analyzeTokenRef`) holding the bearer token. */
|
|
176
|
+
tokenEnv: string
|
|
177
|
+
/** Token status; absent while the DSH composition has no credential store. */
|
|
178
|
+
token?: TokenStatusWire
|
|
179
|
+
topics: TopicsAvailability
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
/** `GET /guanxiang/state` response. */
|
|
150
183
|
export interface StateWire {
|
|
151
184
|
/** Effective settings: cordis.yml values with panel overrides applied. */
|
|
@@ -158,6 +191,7 @@ export interface StateWire {
|
|
|
158
191
|
overlayError?: string
|
|
159
192
|
repos: RepoStatusWire[]
|
|
160
193
|
credentials: CredentialsWire
|
|
194
|
+
analyze: AnalyzeWire
|
|
161
195
|
}
|
|
162
196
|
|
|
163
197
|
/** Successful mutation response. */
|