@v587d/capital-generation 2.15.1 → 2.16.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +7 -7
  3. package/automation/builtins.js +3 -3
  4. package/automation/client.js +47 -17
  5. package/automation/client.src.cjs +294 -83
  6. package/automation/contract.js +4 -0
  7. package/automation/defaults.js +2 -0
  8. package/automation/index.js +11 -7
  9. package/automation/package.json +2 -1
  10. package/automation/route.js +8 -1
  11. package/automation/runner.js +28 -6
  12. package/automation/service.js +89 -0
  13. package/automation/settings.js +6 -0
  14. package/automation/workspaces.js +113 -13
  15. package/chart-ui/package.json +1 -1
  16. package/config/client.js +12 -5
  17. package/config/client.src.cjs +223 -0
  18. package/config/index.js +2 -0
  19. package/config/package.json +1 -1
  20. package/lib/agents/root-tool-policy.d.ts +3 -3
  21. package/lib/agents/root-tool-policy.js +4 -2
  22. package/lib/data-collector/dataset-tools.js +22 -0
  23. package/lib/data-collector/describe.js +1 -0
  24. package/lib/data-collector/hub.js +5 -0
  25. package/lib/data-collector/store.d.ts +5 -0
  26. package/lib/data-collector/store.js +17 -0
  27. package/lib/data-collector/time-axis.d.ts +15 -0
  28. package/lib/data-collector/time-axis.js +73 -0
  29. package/lib/data-collector/tools.d.ts +1 -1
  30. package/lib/data-collector/tools.js +12 -1
  31. package/lib/index.d.ts +5 -0
  32. package/lib/index.js +2 -0
  33. package/lib/sources/tencent-http.js +7 -2
  34. package/lib/sources/tencent-public-core.d.ts +2 -0
  35. package/lib/sources/tencent-public-core.js +5 -0
  36. package/lib/sources/wind-mcp.js +3 -3
  37. package/lib/sources-entry.d.ts +1 -1
  38. package/lib/web-retriever/local-fetch.d.ts +1 -1
  39. package/lib/web-retriever/local-fetch.js +1 -1
  40. package/lib/web-retriever/sources.d.ts +59 -0
  41. package/lib/web-retriever/sources.js +294 -0
  42. package/lib/web-retriever/tools.js +202 -9
  43. package/lib/web-retriever/wind-client.d.ts +1 -1
  44. package/lib/web-retriever/wind-client.js +1 -1
  45. package/package.json +1 -1
  46. package/preset/capital-generation/agent.patch.yml +20 -19
  47. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  48. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  49. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +24 -13
  50. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +39 -16
  51. package/watchlist/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-automation",
3
- "version": "2.15.1",
3
+ "version": "2.16.1",
4
4
  "description": "CapGen 投研自动化:独立 host bundle(固定 capital_automation 域、单 scheduler owner、/capital-automation 路由与浏览器页面)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,6 +41,7 @@
41
41
  "runner.js",
42
42
  "scheduler.js",
43
43
  "workspaces.js",
44
+ "contract.js",
44
45
  "policy.js",
45
46
  "route.js",
46
47
  "settings.js",
@@ -6,8 +6,9 @@ import { MAX_REQUEST_BODY_BYTES } from './defaults.js'
6
6
  import { AutomationError } from './service.js'
7
7
  import { SchemaError } from './schema.js'
8
8
  import { sanitizeMessage } from './redact.js'
9
+ import { AUTOMATION_ROUTE, WORKSPACES_SUFFIX } from './contract.js'
9
10
 
10
- export const ROUTE_PREFIX = '/capital-automation'
11
+ export const ROUTE_PREFIX = AUTOMATION_ROUTE
11
12
  const ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/
12
13
 
13
14
  function send(res, status, payload, extraHeaders = {}) {
@@ -226,9 +227,15 @@ async function dispatch({ req, method, suffix, url, service, requestId }) {
226
227
  if (method === 'DELETE') return { body: await service.deleteTask(id) }
227
228
  throw new AutomationError('method_not_allowed', '只支持 GET / PATCH / DELETE。', 405)
228
229
  }
230
+ if (suffix === WORKSPACES_SUFFIX && method === 'GET') {
231
+ return { body: await service.workspaceCatalog() }
232
+ }
229
233
  if (suffix === '/runs' && method === 'GET') {
230
234
  return { body: await service.listRuns(queryOf(url, new Set(['limit', 'before']))) }
231
235
  }
236
+ if (suffix === '/runs/history' && method === 'GET') {
237
+ return { body: await service.runHistory(queryOf(url, new Set(['days', 'tz']))) }
238
+ }
232
239
  if (suffix === '/notifications' && method === 'GET') {
233
240
  return { body: await service.listNotifications(queryOf(url, new Set(['unread', 'scope']))) }
234
241
  }
@@ -6,6 +6,7 @@
6
6
  import { mkdir } from 'node:fs/promises'
7
7
  import { CANCEL_GRACE_MS, MAX_ERRORS_PER_RUN, QUIET_SETTLE_MS, SETTLED_RUN_STATUSES } from './defaults.js'
8
8
  import { sanitizeMessage } from './redact.js'
9
+ import { localPartsAt } from './time.js'
9
10
 
10
11
  const PRESET_ID = 'capital-generation'
11
12
  const POLICY_ID = 'automation-v1'
@@ -39,11 +40,28 @@ function iso(now) {
39
40
  }
40
41
 
41
42
  function shortRunId(id) {
42
- return String(id).replace(/^run_/, '').slice(-8) || 'unknown'
43
+ return String(id).replace(/^run_/, '').slice(0, 8) || 'unknown'
43
44
  }
44
45
 
45
- export function sessionTitleOf(task, profileName, runId) {
46
- const raw = `[CapGen 投研自动化·${profileName}] ${task.title}·${shortRunId(runId)}`
46
+ function pad2(value) {
47
+ return String(value).padStart(2, '0')
48
+ }
49
+
50
+ /**
51
+ * Session title is `<mm-dd> <task title> <run id prefix>` in the trigger's zone, so the
52
+ * sidebar reads chronologically without repeating the automation profile name.
53
+ */
54
+ export function sessionTitleOf(task, runId, at = Date.now()) {
55
+ const instant = typeof at === 'string' ? Date.parse(at) : Number(at)
56
+ const time = Number.isFinite(instant) ? instant : Date.now()
57
+ let monthDay
58
+ try {
59
+ const { month, day } = localPartsAt(time, task.trigger.timezone)
60
+ monthDay = `${pad2(month)}-${pad2(day)}`
61
+ } catch {
62
+ monthDay = '00-00'
63
+ }
64
+ const raw = `${monthDay} ${task.title} ${shortRunId(runId)}`
47
65
  return raw.length > 200 ? `${raw.slice(0, 197)}...` : raw
48
66
  }
49
67
 
@@ -53,7 +71,7 @@ function promptData(value) {
53
71
  }
54
72
 
55
73
  export function buildRunPrompt({ task, run, trigger, profileName, now }) {
56
- const local = new Intl.DateTimeFormat('zh-CN', {
74
+ const formatLocal = (instant) => new Intl.DateTimeFormat('zh-CN', {
57
75
  timeZone: task.trigger.timezone,
58
76
  year: 'numeric',
59
77
  month: '2-digit',
@@ -61,7 +79,9 @@ export function buildRunPrompt({ task, run, trigger, profileName, now }) {
61
79
  hour: '2-digit',
62
80
  minute: '2-digit',
63
81
  hourCycle: 'h23',
64
- }).format(new Date(now))
82
+ }).format(new Date(instant))
83
+ const local = formatLocal(now)
84
+ const deadline = formatLocal(Number(now) + task.limits.runTimeoutMinutes * 60_000)
65
85
  const reason = trigger === 'manual' ? '手动运行' : '定时触发'
66
86
  return `【CapGen 投研自动化】
67
87
  运行: ${run.id}
@@ -69,6 +89,7 @@ export function buildRunPrompt({ task, run, trigger, profileName, now }) {
69
89
  触发: ${reason}
70
90
  occurrence: ${run.occurrenceKey}
71
91
  ${run.scheduledFor === undefined ? '' : `计划时刻: ${run.scheduledFor}\n`}当前时间: ${local}(${task.trigger.timezone})
92
+ 最晚结束: ${deadline}(${task.trigger.timezone})
72
93
  profile: ${promptData(profileName)}
73
94
  任务分类: ${task.category}
74
95
  无人值守策略: ${POLICY_ID}
@@ -88,6 +109,7 @@ ${task.instruction}
88
109
  - 这是无人值守运行:禁止 ask_user_question、禁止等待审批、禁止写文件或提权。
89
110
  - 使用当前角色可见的工具面;结构化数据交 data_collector,网页材料交 web_retriever,分析交 data_junior。
90
111
  - 墙钟预算覆盖整棵运行子树;预算过半后优先完成主交付,剩余时间再补图表/可选材料,不要为可选图表或对账耗尽预算。
112
+ - 预算判断只以宿主时钟为准:本 prompt 的「当前时间」是起点,「最晚结束」是硬截止;不要用步数、等待次数或工具耗时估算。每次决定「继续等待 / 收口 / 改用网页口径」前,先调用 get_local_datetime,用当前时间与最晚结束计算剩余时间;剩余时间足够就继续等数据,不够才收口。
91
113
  - 等待子 Agent 用 wait_for_agents;不要用 bash sleep 代替等待,它会阻塞回传处理,也不会让子 Agent 更快收敛。
92
114
  - 最终答复必须自足:结论、证据与来源、局限与风险;不要写“见上”“已完成”。
93
115
  - 缺少关键输入时,采用最保守的可核查口径继续,并在开头写明「需要用户确认」及具体缺口。`
@@ -777,7 +799,7 @@ export function createRunRunner(options) {
777
799
  await stopEntryAgent(entry, controller)
778
800
  return
779
801
  }
780
- const title = sessionTitleOf(task, profile.displayName, run.id)
802
+ const title = sessionTitleOf(task, run.id, run.queuedAt)
781
803
  const renamed = controller.rename === undefined
782
804
  ? undefined
783
805
  : await controller.rename({ sessionId: created.sessionId, title }).catch(() => undefined)
@@ -4,7 +4,9 @@
4
4
  * profile filter; the wire body never selects a profile.
5
5
  */
6
6
  import {
7
+ ACTIVE_RUN_STATUSES,
7
8
  DEFAULT_AUTOMATION_SETTINGS,
9
+ FAILURE_RUN_STATUSES,
8
10
  MAX_IDLE_TIMEOUT_MINUTES,
9
11
  MAX_MAX_STEPS,
10
12
  MAX_MAX_TOOL_CALLS,
@@ -22,14 +24,49 @@ import {
22
24
  parseStrict,
23
25
  } from './schema.js'
24
26
  import { sanitizeMessage } from './redact.js'
27
+ import { addDays, localPartsAt } from './time.js'
25
28
 
26
29
  const MAX_RUN_PAGE_LIMIT = 2_000
27
30
  const RUN_CURSOR_PATTERN = /^[A-Za-z0-9_-]{1,120}$/
31
+ const HISTORY_DAYS = 371
32
+ const ACTIVE_STATUSES = new Set(ACTIVE_RUN_STATUSES)
33
+ const FAILURE_STATUSES = new Set(FAILURE_RUN_STATUSES)
28
34
 
29
35
  function invalidQuery(message) {
30
36
  return new AutomationError('invalid_query', message, 400)
31
37
  }
32
38
 
39
+ function historyDaysOf(value) {
40
+ if (value === undefined) return HISTORY_DAYS
41
+ if (typeof value !== 'string' && typeof value !== 'number') throw invalidQuery('运行历史 days 参数无效。')
42
+ const text = String(value)
43
+ if (!/^\d{1,3}$/.test(text)) throw invalidQuery(`运行历史 days 必须是 1–${HISTORY_DAYS} 的整数。`)
44
+ const days = Number(text)
45
+ if (!Number.isSafeInteger(days) || days < 1 || days > HISTORY_DAYS) {
46
+ throw invalidQuery(`运行历史 days 必须是 1–${HISTORY_DAYS} 的整数。`)
47
+ }
48
+ return days
49
+ }
50
+
51
+ function historyTimeZoneOf(value) {
52
+ if (value === undefined) {
53
+ try {
54
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
55
+ } catch {
56
+ return 'UTC'
57
+ }
58
+ }
59
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64) {
60
+ throw invalidQuery('运行历史 tz 参数无效。')
61
+ }
62
+ try {
63
+ new Intl.DateTimeFormat('en-US', { timeZone: value })
64
+ } catch {
65
+ throw invalidQuery('运行历史 tz 参数无效。')
66
+ }
67
+ return value
68
+ }
69
+
33
70
  function notificationScopeOf(value) {
34
71
  if (value === undefined) return 'task'
35
72
  if (value === 'task' || value === 'system' || value === 'all') return value
@@ -165,6 +202,15 @@ export function createAutomationService(options) {
165
202
  }
166
203
  }
167
204
 
205
+ async function workspaceCatalog() {
206
+ if (typeof workspaces?.details !== 'function') return { workspaces: [], defaultWorkspaceId: null }
207
+ try {
208
+ return await workspaces.details()
209
+ } catch (error) {
210
+ throw new AutomationError('workspace_unavailable', sanitizeMessage(String(error?.message ?? error)), 503)
211
+ }
212
+ }
213
+
168
214
  async function createTask(input) {
169
215
  const parsed = parseStrict(TaskCreateSchema, input, 'task')
170
216
  const defaults = { ...DEFAULT_AUTOMATION_SETTINGS, ...settings() }
@@ -285,6 +331,47 @@ export function createAutomationService(options) {
285
331
  }
286
332
  }
287
333
 
334
+ /**
335
+ * Daily run counts for the contribution heatmap. Days are bucketed in the browser's zone,
336
+ * because `queuedAt` is an instant and the calendar day is a local fact.
337
+ */
338
+ async function runHistory(query = {}) {
339
+ const days = historyDaysOf(query.days)
340
+ const timeZone = historyTimeZoneOf(query.tz)
341
+ const end = localPartsAt(now(), timeZone)
342
+ const start = addDays(end, -(days - 1))
343
+ const buckets = []
344
+ const byDate = new Map()
345
+ for (let index = 0; index < days; index += 1) {
346
+ const date = addDays(start, index).date
347
+ const bucket = { date, total: 0, success: 0, failure: 0, active: 0 }
348
+ buckets.push(bucket)
349
+ byDate.set(date, bucket)
350
+ }
351
+ const page = await store.listRunsPage({ limit: MAX_RUN_PAGE_LIMIT })
352
+ let total = 0
353
+ let success = 0
354
+ let failure = 0
355
+ for (const run of page.runs) {
356
+ const at = Date.parse(run.queuedAt)
357
+ if (Number.isNaN(at)) continue
358
+ const bucket = byDate.get(localPartsAt(at, timeZone).date)
359
+ if (bucket === undefined) continue
360
+ bucket.total += 1
361
+ total += 1
362
+ if (run.status === 'succeeded') {
363
+ bucket.success += 1
364
+ success += 1
365
+ }
366
+ if (FAILURE_STATUSES.has(run.status)) {
367
+ bucket.failure += 1
368
+ failure += 1
369
+ }
370
+ if (ACTIVE_STATUSES.has(run.status)) bucket.active += 1
371
+ }
372
+ return { days: buckets, total, success, failure }
373
+ }
374
+
288
375
  async function listNotifications(query = {}) {
289
376
  let unreadOnly = false
290
377
  if (query.unread !== undefined) {
@@ -316,6 +403,7 @@ export function createAutomationService(options) {
316
403
  return {
317
404
  serialize,
318
405
  meta,
406
+ workspaceCatalog,
319
407
  listTasks,
320
408
  taskDetail,
321
409
  createTask,
@@ -327,6 +415,7 @@ export function createAutomationService(options) {
327
415
  cancelRun,
328
416
  getRun,
329
417
  listRuns,
418
+ runHistory,
330
419
  listNotifications,
331
420
  markNotificationsRead,
332
421
  storageState: async () => {
@@ -26,6 +26,11 @@ function intIn(value, minimum, maximum, fallback) {
26
26
 
27
27
  export function normalizeAutomationSettings(value = {}, fallback = DEFAULT_AUTOMATION_SETTINGS) {
28
28
  const source = value ?? {}
29
+ const workspaceId = source.defaultWorkspaceId === undefined
30
+ ? fallback.defaultWorkspaceId
31
+ : typeof source.defaultWorkspaceId === 'string'
32
+ ? source.defaultWorkspaceId.trim()
33
+ : fallback.defaultWorkspaceId
29
34
  return {
30
35
  tickMs: intIn(source.tickMs, MIN_TICK_MS, MAX_TICK_MS, fallback.tickMs),
31
36
  runTimeoutMinutes: intIn(source.runTimeoutMinutes, MIN_RUN_TIMEOUT_MINUTES, MAX_RUN_TIMEOUT_MINUTES, fallback.runTimeoutMinutes),
@@ -33,6 +38,7 @@ export function normalizeAutomationSettings(value = {}, fallback = DEFAULT_AUTOM
33
38
  maxSteps: intIn(source.maxSteps, MIN_MAX_STEPS, MAX_MAX_STEPS, fallback.maxSteps),
34
39
  maxToolCalls: intIn(source.maxToolCalls, MIN_MAX_TOOL_CALLS, MAX_MAX_TOOL_CALLS, fallback.maxToolCalls),
35
40
  inAppNotifications: source.inAppNotifications === undefined ? fallback.inAppNotifications : source.inAppNotifications !== false,
41
+ defaultWorkspaceId: workspaceId ?? '',
36
42
  }
37
43
  }
38
44
 
@@ -4,6 +4,9 @@
4
4
  * sessions are grouped instead of landing in the ungrouped bucket.
5
5
  */
6
6
  import { mkdir, realpath } from 'node:fs/promises'
7
+ import { join, sep } from 'node:path'
8
+
9
+ export const AUTOMATION_WORKSPACE_DIR = 'CapGen-automation-workspace'
7
10
 
8
11
  export class WorkspaceError extends Error {
9
12
  constructor(code, message) {
@@ -15,6 +18,7 @@ export class WorkspaceError extends Error {
15
18
 
16
19
  const DEFAULT_REGISTRATIONS = new WeakMap()
17
20
  const MAX_SESSIONS_TO_ADOPT = 500
21
+ const MAX_WORKSPACE_ID_CHARS = 200
18
22
 
19
23
  function service(ctx, name) {
20
24
  try {
@@ -24,15 +28,54 @@ function service(ctx, name) {
24
28
  }
25
29
  }
26
30
 
31
+ /** Directory segment safe on every platform; the raw profile name never becomes a path. */
32
+ export function safeSegment(value) {
33
+ return String(value).replace(/[^A-Za-z0-9._-]+/g, '_').slice(0, 120)
34
+ }
35
+
36
+ export function defaultWorkspacePath(profile = {}) {
37
+ const home = profile.home ?? profile.dir ?? process.cwd()
38
+ return join(home, AUTOMATION_WORKSPACE_DIR, safeSegment(profile.name ?? 'default'))
39
+ }
40
+
41
+ export function defaultWorkspaceTitle(displayName) {
42
+ return `CapGen 投研自动化(${displayName})`
43
+ }
44
+
45
+ /** `~` only for paths under the user's home; anything else stays absolute so it is unambiguous. */
46
+ export function workspaceDisplayPath(path, home) {
47
+ if (typeof path !== 'string' || typeof home !== 'string' || home.length === 0) return path
48
+ const prefix = home.endsWith(sep) ? home : `${home}${sep}`
49
+ return path.startsWith(prefix) ? `~${path.slice(home.length)}` : path
50
+ }
51
+
27
52
  export function createWorkspaceCatalog(options) {
28
- const { ctx, displayName, runDir, logger } = options
29
- let defaultEntity
30
- let defaultPromise
53
+ const { ctx, displayName, runDir, displayHome, getDefaultWorkspaceId, logger } = options
54
+ let dedicatedEntity
55
+ let dedicatedPromise
56
+ let warnedDefaultId
31
57
 
32
58
  function registry() {
33
59
  return service(ctx, 'workspaceRegistry')
34
60
  }
35
61
 
62
+ function configuredDefaultId() {
63
+ let value
64
+ try {
65
+ value = typeof getDefaultWorkspaceId === 'function' ? getDefaultWorkspaceId() : getDefaultWorkspaceId
66
+ } catch {
67
+ value = undefined
68
+ }
69
+ if (typeof value !== 'string') return ''
70
+ return value.trim().slice(0, MAX_WORKSPACE_ID_CHARS)
71
+ }
72
+
73
+ function warnDefaultUnusable(id, reason) {
74
+ if (warnedDefaultId === id) return
75
+ warnedDefaultId = id
76
+ logger?.warn?.(`capital-automation: 默认工作区 ${id} ${reason},回退到专属默认工作区。`)
77
+ }
78
+
36
79
  async function adoptSessions(entity) {
37
80
  if (entity.sessionIds.length > 0) return
38
81
  const persistence = service(ctx, 'sessionPersistence')
@@ -66,8 +109,8 @@ export function createWorkspaceCatalog(options) {
66
109
  }
67
110
 
68
111
  async function ensureDefault() {
69
- if (defaultEntity !== undefined) return defaultEntity
70
- if (defaultPromise !== undefined) return defaultPromise
112
+ if (dedicatedEntity !== undefined) return dedicatedEntity
113
+ if (dedicatedPromise !== undefined) return dedicatedPromise
71
114
  const current = registry()
72
115
  if (current === undefined) return undefined
73
116
  let registrations = DEFAULT_REGISTRATIONS.get(current)
@@ -85,7 +128,7 @@ export function createWorkspaceCatalog(options) {
85
128
  }
86
129
  if (created === undefined) {
87
130
  try {
88
- created = await current.create(runDir, `CapGen 投研自动化(${displayName})`)
131
+ created = await current.create(runDir, defaultWorkspaceTitle(displayName))
89
132
  } catch (error) {
90
133
  const recovered = typeof current.resolveByPath === 'function'
91
134
  ? await current.resolveByPath(runDir).catch(() => undefined)
@@ -94,35 +137,92 @@ export function createWorkspaceCatalog(options) {
94
137
  created = recovered
95
138
  }
96
139
  }
97
- defaultEntity = created
140
+ dedicatedEntity = created
98
141
  await adoptSessions(created)
99
142
  return created
100
143
  })()
101
144
  registrations.set(runDir, sharedPromise)
102
145
  }
103
- defaultPromise = sharedPromise
146
+ dedicatedPromise = sharedPromise
104
147
  try {
105
- return await sharedPromise
148
+ const created = await sharedPromise
149
+ if (created !== undefined) dedicatedEntity = created
150
+ return created
106
151
  } catch (error) {
107
152
  logger?.warn?.(`capital-automation: 无法注册默认工作区:${String(error)}`)
108
153
  return undefined
109
154
  } finally {
110
155
  if (registrations.get(runDir) === sharedPromise) registrations.delete(runDir)
111
- if (defaultEntity === undefined) defaultPromise = undefined
156
+ if (dedicatedEntity === undefined) dedicatedPromise = undefined
112
157
  }
113
158
  }
114
159
 
160
+ /**
161
+ * A configured default is honoured only while it is registered and its directory is usable.
162
+ * A stale id must degrade to the dedicated workspace instead of breaking every empty task.
163
+ */
164
+ async function configuredDefaultEntity() {
165
+ const id = configuredDefaultId()
166
+ if (id === '') return undefined
167
+ const entity = registry()?.get?.(id)
168
+ if (entity === undefined) {
169
+ warnDefaultUnusable(id, '不存在或已被移除')
170
+ return undefined
171
+ }
172
+ let status
173
+ try {
174
+ status = await entity.status?.()
175
+ } catch {
176
+ status = 'unavailable'
177
+ }
178
+ if (status !== undefined && status !== 'ok') {
179
+ warnDefaultUnusable(id, '目录不可用')
180
+ return undefined
181
+ }
182
+ warnedDefaultId = undefined
183
+ return entity
184
+ }
185
+
186
+ async function effectiveDefault() {
187
+ const configured = await configuredDefaultEntity()
188
+ if (configured !== undefined) return configured
189
+ return ensureDefault()
190
+ }
191
+
115
192
  async function list() {
193
+ await ensureDefault()
116
194
  const current = registry()
117
195
  if (current === undefined) return []
118
- const preferred = await ensureDefault()
196
+ const preferred = await effectiveDefault()
197
+ const dedicatedId = dedicatedEntity?.id
119
198
  return current.list().map((entity) => ({
120
199
  id: entity.id,
121
200
  title: entity.title,
122
201
  default: preferred !== undefined && entity.id === preferred.id,
202
+ ...(entity.id === dedicatedId ? { dedicated: true } : {}),
123
203
  }))
124
204
  }
125
205
 
206
+ /** Settings-card projection only: it carries paths, unlike the task page's `/meta`. */
207
+ async function details() {
208
+ await ensureDefault()
209
+ const current = registry()
210
+ if (current === undefined) return { workspaces: [], defaultWorkspaceId: null }
211
+ const preferred = await effectiveDefault()
212
+ const dedicatedId = dedicatedEntity?.id
213
+ return {
214
+ workspaces: current.list().map((entity) => ({
215
+ id: entity.id,
216
+ title: entity.title,
217
+ path: entity.path,
218
+ displayPath: workspaceDisplayPath(entity.path, displayHome),
219
+ default: preferred !== undefined && entity.id === preferred.id,
220
+ dedicated: entity.id === dedicatedId,
221
+ })),
222
+ defaultWorkspaceId: preferred?.id ?? null,
223
+ }
224
+ }
225
+
126
226
  async function validate(id) {
127
227
  if (typeof id !== 'string' || id.length === 0) return
128
228
  const current = registry()
@@ -145,10 +245,10 @@ export function createWorkspaceCatalog(options) {
145
245
  const entity = registry().get(id)
146
246
  return { workspaceId: entity.id, path: entity.path }
147
247
  }
148
- const preferred = await ensureDefault()
248
+ const preferred = await effectiveDefault()
149
249
  if (preferred !== undefined) return { workspaceId: preferred.id, path: preferred.path }
150
250
  return { workspaceId: undefined, path: runDir }
151
251
  }
152
252
 
153
- return { list, validate, resolve, runDir }
253
+ return { list, details, validate, resolve, runDir }
154
254
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-charts",
3
- "version": "2.15.1",
3
+ "version": "2.16.1",
4
4
  "description": "自动模式的图表前端组件(浏览器半边 + 序列旁路路由):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",