@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.
- package/CHANGELOG.md +90 -0
- package/README.md +7 -7
- package/automation/builtins.js +3 -3
- package/automation/client.js +47 -17
- package/automation/client.src.cjs +294 -83
- package/automation/contract.js +4 -0
- package/automation/defaults.js +2 -0
- package/automation/index.js +11 -7
- package/automation/package.json +2 -1
- package/automation/route.js +8 -1
- package/automation/runner.js +28 -6
- package/automation/service.js +89 -0
- package/automation/settings.js +6 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/package.json +1 -1
- package/config/client.js +12 -5
- package/config/client.src.cjs +223 -0
- package/config/index.js +2 -0
- package/config/package.json +1 -1
- package/lib/agents/root-tool-policy.d.ts +3 -3
- package/lib/agents/root-tool-policy.js +4 -2
- package/lib/data-collector/dataset-tools.js +22 -0
- package/lib/data-collector/describe.js +1 -0
- package/lib/data-collector/hub.js +5 -0
- package/lib/data-collector/store.d.ts +5 -0
- package/lib/data-collector/store.js +17 -0
- package/lib/data-collector/time-axis.d.ts +15 -0
- package/lib/data-collector/time-axis.js +73 -0
- package/lib/data-collector/tools.d.ts +1 -1
- package/lib/data-collector/tools.js +12 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +2 -0
- package/lib/sources/tencent-http.js +7 -2
- package/lib/sources/tencent-public-core.d.ts +2 -0
- package/lib/sources/tencent-public-core.js +5 -0
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/sources-entry.d.ts +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/sources.d.ts +59 -0
- package/lib/web-retriever/sources.js +294 -0
- package/lib/web-retriever/tools.js +202 -9
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +1 -1
- package/preset/capital-generation/agent.patch.yml +20 -19
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +24 -13
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +39 -16
- package/watchlist/package.json +1 -1
package/automation/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-automation",
|
|
3
|
-
"version": "2.
|
|
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",
|
package/automation/route.js
CHANGED
|
@@ -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 =
|
|
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
|
}
|
package/automation/runner.js
CHANGED
|
@@ -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(
|
|
43
|
+
return String(id).replace(/^run_/, '').slice(0, 8) || 'unknown'
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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)
|
package/automation/service.js
CHANGED
|
@@ -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 () => {
|
package/automation/settings.js
CHANGED
|
@@ -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
|
|
package/automation/workspaces.js
CHANGED
|
@@ -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
|
|
30
|
-
let
|
|
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 (
|
|
70
|
-
if (
|
|
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,
|
|
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
|
-
|
|
140
|
+
dedicatedEntity = created
|
|
98
141
|
await adoptSessions(created)
|
|
99
142
|
return created
|
|
100
143
|
})()
|
|
101
144
|
registrations.set(runDir, sharedPromise)
|
|
102
145
|
}
|
|
103
|
-
|
|
146
|
+
dedicatedPromise = sharedPromise
|
|
104
147
|
try {
|
|
105
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
}
|