@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/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, and the settings routes, then
54
- * clone or update the configured repositories in the background.
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 type { CredentialsWire, EditableSettings, StateWire, TokenStatusWire } from './settings/contract.ts'
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(async (tokenEnv): Promise<TokenStatusWire> => {
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 not referenced by the saved settings, or no credential store is loaded.
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
+ }
@@ -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. */