agentworkshop 0.7.12 → 0.7.14

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 (100) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/CD1SlCoI.js +14 -0
  3. package/.output/public/_nuxt/CYBq3kS7.js +1 -0
  4. package/.output/public/_nuxt/Cr9DkD1d.js +1 -0
  5. package/.output/public/_nuxt/DGVAuScD.js +1 -0
  6. package/.output/public/_nuxt/DMdVWIGz.js +19 -0
  7. package/.output/public/_nuxt/DVoWWlUW.js +1 -0
  8. package/.output/public/_nuxt/agents.CB9Ck7VQ.css +1 -0
  9. package/.output/public/_nuxt/builds/latest.json +1 -1
  10. package/.output/public/_nuxt/builds/meta/23c30854-f9e1-4651-bc3a-b695ef5faca4.json +1 -0
  11. package/.output/public/_nuxt/channel-templates.C-M5nKcC.css +1 -0
  12. package/.output/public/_nuxt/{Ds8JbqGB.js → ed5qX-x5.js} +1 -1
  13. package/.output/public/_nuxt/{entry.Bv_GCmPP.css → entry.BNApuuEW.css} +1 -1
  14. package/.output/public/_nuxt/logs.BarkrvUp.css +1 -0
  15. package/.output/public/_nuxt/pages.CG8UJ2TK.css +1 -0
  16. package/.output/public/_nuxt/teams.DN0pAGGu.css +1 -0
  17. package/.output/public/_nuxt/tokens.BVXU1jOK.css +1 -0
  18. package/.output/server/chunks/_/nitro.mjs +849 -752
  19. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  20. package/.output/server/chunks/_/user.schema.mjs +1 -1
  21. package/.output/server/chunks/_/user.schema.mjs.map +1 -1
  22. package/.output/server/chunks/build/TownView-CunmEMzS.mjs +1 -1
  23. package/.output/server/chunks/build/_id_-CVnMdSeG.mjs +1 -1
  24. package/.output/server/chunks/build/_id_-D265Wh_k.mjs +1 -1
  25. package/.output/server/chunks/build/{agents-B10wmFri.mjs → agents--6bzGtES.mjs} +156 -80
  26. package/.output/server/chunks/build/agents--6bzGtES.mjs.map +1 -0
  27. package/.output/server/chunks/build/{channel-templates-CpWxVk5A.mjs → channel-templates-C1Bb-U75.mjs} +19 -20
  28. package/.output/server/chunks/build/channel-templates-C1Bb-U75.mjs.map +1 -0
  29. package/.output/server/chunks/build/daq-DxaXk_8N.mjs +1 -1
  30. package/.output/server/chunks/build/dcw-B0dtiHFG.mjs +1 -1
  31. package/.output/server/chunks/build/{logs-Crf6f1IC.mjs → logs-UWXeNKy_.mjs} +34 -34
  32. package/.output/server/chunks/build/{logs-Crf6f1IC.mjs.map → logs-UWXeNKy_.mjs.map} +1 -1
  33. package/.output/server/chunks/build/{pages-BmRUbGUi.mjs → pages-Aexs6LjD.mjs} +27 -15
  34. package/.output/server/chunks/build/pages-Aexs6LjD.mjs.map +1 -0
  35. package/.output/server/chunks/build/{teams-CNflUtPh.mjs → teams-DD8ZXTKL.mjs} +29 -32
  36. package/.output/server/chunks/build/teams-DD8ZXTKL.mjs.map +1 -0
  37. package/.output/server/chunks/build/{tokens-BKLzcdMg.mjs → tokens-Dxnuw96_.mjs} +14 -14
  38. package/.output/server/chunks/build/{tokens-BKLzcdMg.mjs.map → tokens-Dxnuw96_.mjs.map} +1 -1
  39. package/.output/server/chunks/build/useDaqStream-BkmyQT06.mjs +1 -1
  40. package/.output/server/chunks/build/useDcwStream-DWVoU96J.mjs +1 -1
  41. package/.output/server/chunks/build/useWorkshopApi-D2gq06N2.mjs.map +1 -1
  42. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  43. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
  44. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  45. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +14 -2
  46. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs.map +1 -1
  47. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  48. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  49. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  50. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  51. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  52. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  53. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  54. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  55. package/.output/server/chunks/routes/renderer.mjs +2 -2
  56. package/.output/server/chunks/virtual/entry.mjs +25 -13
  57. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  58. package/.output/server/package.json +1 -1
  59. package/app/composables/workshop/useWorkshopApi.ts +11 -1
  60. package/app/pages/index.vue +72 -0
  61. package/app/pages/logs/index.vue +3 -1
  62. package/app/pages/tokens.vue +3 -1
  63. package/app/pages/workshop/agents.vue +71 -6
  64. package/app/pages/workshop/channel-templates.vue +2 -1
  65. package/app/pages/workshop/teams.vue +24 -2
  66. package/app/plugins/http.ts +4 -3
  67. package/i18n/locales/en.ts +8 -1
  68. package/i18n/locales/zh-CN.ts +8 -1
  69. package/package.json +148 -148
  70. package/scripts/_dbg-harness-e2e.mjs +92 -0
  71. package/scripts/_dbg-harness-ui-dbg.py +49 -0
  72. package/scripts/_dbg-harness-ui-e2e.py +87 -0
  73. package/scripts/_dbg-login-e2e.mjs +26 -0
  74. package/server/api/workshop/harnesses.get.ts +15 -2
  75. package/server/data/dcw-lines.json +14 -0
  76. package/server/data/dcw-rollback.json +6 -6
  77. package/server/data/dcws.json +198 -198
  78. package/server/repositories/user.repository.ts +4 -3
  79. package/server/schemas/user.schema.ts +2 -2
  80. package/server/services/workshop/agents/adapters/line-spawn.ts +3 -2
  81. package/server/services/workshop/agents/harness-availability.ts +127 -0
  82. package/server/services/workshop/agents/registry.ts +21 -0
  83. package/server/services/workshop/runtime/manager.ts +16 -1
  84. package/.output/public/_nuxt/CvYfzHYz.js +0 -1
  85. package/.output/public/_nuxt/D2JyEL28.js +0 -14
  86. package/.output/public/_nuxt/D8xY-Yjc.js +0 -19
  87. package/.output/public/_nuxt/QoCzfpo2.js +0 -1
  88. package/.output/public/_nuxt/agents.CEbox5-D.css +0 -1
  89. package/.output/public/_nuxt/builds/meta/54e22640-4fd0-4fee-9077-31d4d4bb7969.json +0 -1
  90. package/.output/public/_nuxt/channel-templates.De5ZvrGz.css +0 -1
  91. package/.output/public/_nuxt/eUGyJdiT.js +0 -1
  92. package/.output/public/_nuxt/hb_ceMJP.js +0 -1
  93. package/.output/public/_nuxt/logs.BS1sb41K.css +0 -1
  94. package/.output/public/_nuxt/pages.BkE-AfIb.css +0 -1
  95. package/.output/public/_nuxt/teams.CUMnbQGf.css +0 -1
  96. package/.output/public/_nuxt/tokens.rW1xHxRW.css +0 -1
  97. package/.output/server/chunks/build/agents-B10wmFri.mjs.map +0 -1
  98. package/.output/server/chunks/build/channel-templates-CpWxVk5A.mjs.map +0 -1
  99. package/.output/server/chunks/build/pages-BmRUbGUi.mjs.map +0 -1
  100. package/.output/server/chunks/build/teams-CNflUtPh.mjs.map +0 -1
@@ -319,10 +319,11 @@ export const userRepository = {
319
319
  return new Map(rows.map(r => [r.lineId, r.mode]))
320
320
  },
321
321
 
322
- /** 内部:取密码哈希(仅认证路径使用,不参与领域对象外泄) */
323
- getPasswordHash(email: string): { id: string, hash: string } | null {
322
+ /** 内部:取密码哈希(仅认证路径使用,不参与领域对象外泄)。
323
+ * 登录账号:邮箱或用户名均可(用户名注册时唯一;参数绑定防注入) */
324
+ getPasswordHash(account: string): { id: string, hash: string } | null {
324
325
  const d = getDb()
325
- const row = d.prepare('SELECT id, password_hash AS hash FROM users WHERE LOWER(email) = LOWER(?)').get(email) as { id: string, hash: string } | undefined
326
+ const row = d.prepare('SELECT id, password_hash AS hash FROM users WHERE LOWER(email) = LOWER(?) OR LOWER(name) = LOWER(?)').get(account, account) as { id: string, hash: string } | undefined
326
327
  return row ?? null
327
328
  },
328
329
 
@@ -42,9 +42,9 @@ export const userRegisterSchema = z.object({
42
42
  password: passwordSchema,
43
43
  })
44
44
 
45
- /** 登录请求体(email + password) */
45
+ /** 登录请求体(account = 邮箱或用户名均可;字段名保留 email 兼容既有客户端) */
46
46
  export const userLoginSchema = z.object({
47
- email: z.string().trim().email('邮箱格式不正确').max(128, '邮箱过长'),
47
+ email: z.string().trim().min(1, '请输入邮箱或用户名').max(128, '过长'),
48
48
  password: z.string().min(1, '密码不能为空').max(128, '密码过长'),
49
49
  })
50
50
 
@@ -24,8 +24,9 @@ export function assertPlainExecutable(command: string): string {
24
24
  return command
25
25
  }
26
26
 
27
- /** PATH + PATHEXT 解析裸命令名 → 绝对路径(win32;非 Windows 返回 null)。带扩展名优先,裸名(sh 脚本)最后 */
28
- function resolveOnPath(command: string): string | null {
27
+ /** PATH + PATHEXT 解析裸命令名 → 绝对路径(win32;非 Windows 返回 null)。带扩展名优先,裸名(sh 脚本)最后。
28
+ * 导出:harness 可用性探测(harness-availability.ts)复用同一解析规则,保证「探测可用」与「真实拉起」同源。 */
29
+ export function resolveOnPath(command: string): string | null {
29
30
  if (process.platform !== 'win32') return null
30
31
  const isFile = (p: string): boolean => {
31
32
  try {
@@ -0,0 +1,127 @@
1
+ /**
2
+ * HarnessAvailability —— 执行引擎环境可用性探测(单一事实源)。
3
+ *
4
+ * 两个消费面共用本模块:
5
+ * - 前端选择面:GET /api/workshop/harnesses 的 available 字段(下拉禁用未安装项/仪表盘面板);
6
+ * - 执行前强校验:manager 的模板增改/克隆入 channel/实例改引擎/任务直发/lead 派发
7
+ * 全部经 assertHarnessUsable,不可用直接 409,任务不再进入「起跑后失败」路径。
8
+ *
9
+ * 探测规则(与真实拉起同源,见 line-spawn.resolveExecutable):
10
+ * - 进程内引擎(mock/claude SDK):无外部 CLI,恒可用;
11
+ * - 进程型引擎(omp/opencode/codex/dsh):经 HarnessDef.probe 解析命令
12
+ * (实例 config.command 覆盖 → 运行时设置缺省),再按 PATH/PATHEXT(unix 加 X_OK)
13
+ * 探测磁盘上的真实可执行文件;找不到 = 未安装。
14
+ * 探测结果按「引擎+命令」缓存 30s(fs 探测廉价但热路径避抖);refresh=1 强制重探。
15
+ */
16
+ import { accessSync, constants, existsSync, statSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+ import { AppError } from '../../../utils/errors'
19
+ import { resolveOnPath } from './adapters/line-spawn'
20
+ import { HARNESS_REGISTRY, knownHarnesses } from './registry'
21
+
22
+ export interface HarnessAvailability {
23
+ id: string
24
+ available: boolean
25
+ /** 进程内引擎(无外部 CLI,恒可用) */
26
+ inprocess: boolean
27
+ /** 进程型引擎:声明将拉起的命令(config 覆盖/设置缺省后) */
28
+ command: string | null
29
+ /** PATH 解析出的可执行文件绝对路径(未找到为 null) */
30
+ resolvedPath: string | null
31
+ /** 不可用原因(人话,可直接展示) */
32
+ error: string | null
33
+ }
34
+
35
+ const isPlainFile = (p: string): boolean => {
36
+ try {
37
+ return existsSync(p) && statSync(p).isFile()
38
+ }
39
+ catch {
40
+ return false
41
+ }
42
+ }
43
+
44
+ const isExecutableFile = (p: string): boolean => {
45
+ try {
46
+ accessSync(p, constants.X_OK)
47
+ return statSync(p).isFile()
48
+ }
49
+ catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ /** PATH 探测可执行文件;探测永不抛(坏 PATH 环境按未安装处理) */
55
+ function probeExecutable(command: string): string | null {
56
+ if (!command) return null
57
+ // 显式路径(含目录分隔符或盘符)不再走 PATH
58
+ if (command.includes('/') || command.includes('\\') || /^[A-Za-z]:/.test(command)) {
59
+ return isPlainFile(command) ? command : null
60
+ }
61
+ if (process.platform === 'win32') return resolveOnPath(command)
62
+ for (const dir of (process.env.PATH ?? '').split(':').filter(Boolean)) {
63
+ const full = join(dir, command)
64
+ if (isExecutableFile(full)) return full
65
+ }
66
+ return null
67
+ }
68
+
69
+ const PROBE_TTL_MS = 30_000
70
+ /** 缓存键 = 引擎+命令(命令不同探测结果不同,不能按引擎 id 独占) */
71
+ const probeCache = new Map<string, { at: number, resolvedPath: string | null }>()
72
+
73
+ export function checkHarnessAvailability(
74
+ harnessId: string,
75
+ config?: Record<string, unknown>,
76
+ opts?: { refresh?: boolean },
77
+ ): HarnessAvailability {
78
+ const def = HARNESS_REGISTRY[harnessId]
79
+ if (!def) {
80
+ return {
81
+ id: harnessId,
82
+ available: false,
83
+ inprocess: false,
84
+ command: null,
85
+ resolvedPath: null,
86
+ error: `未知 harness: ${harnessId}(可选 ${knownHarnesses().join('/')})`,
87
+ }
88
+ }
89
+ const probe = def.probe
90
+ if (!probe || probe.inprocess || !probe.command) {
91
+ return { id: harnessId, available: true, inprocess: true, command: null, resolvedPath: null, error: null }
92
+ }
93
+ const command = probe.command(config)
94
+ const key = `${harnessId}=${command}`
95
+ const cached = probeCache.get(key)
96
+ let resolvedPath: string | null
97
+ if (!opts?.refresh && cached && Date.now() - cached.at < PROBE_TTL_MS) {
98
+ resolvedPath = cached.resolvedPath
99
+ }
100
+ else {
101
+ resolvedPath = probeExecutable(command)
102
+ probeCache.set(key, { at: Date.now(), resolvedPath })
103
+ }
104
+ return {
105
+ id: harnessId,
106
+ available: resolvedPath !== null,
107
+ inprocess: false,
108
+ command,
109
+ resolvedPath,
110
+ error: resolvedPath ? null : `未找到可执行命令「${command}」:引擎 ${def.label} 未安装或不在 PATH 中`,
111
+ }
112
+ }
113
+
114
+ export function checkAllHarnessAvailability(opts?: { refresh?: boolean }): HarnessAvailability[] {
115
+ return Object.keys(HARNESS_REGISTRY).map(id => checkHarnessAvailability(id, undefined, opts))
116
+ }
117
+
118
+ /** 执行前强校验:未知 → 400 UNKNOWN_HARNESS;未安装 → 409 HARNESS_UNAVAILABLE(人话报错,前端可直接展示) */
119
+ export function assertHarnessUsable(harness: string, config?: Record<string, unknown>): void {
120
+ if (!HARNESS_REGISTRY[harness]) {
121
+ throw new AppError(400, 'UNKNOWN_HARNESS', `未知 harness: ${harness}(可选 ${knownHarnesses().join('/')})`)
122
+ }
123
+ const a = checkHarnessAvailability(harness, config)
124
+ if (!a.available) {
125
+ throw new AppError(409, 'HARNESS_UNAVAILABLE', a.error ?? `执行引擎 ${harness} 不可用`)
126
+ }
127
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { AppError } from '../../../utils/errors'
9
9
  import type { AgentInfo, AgentInterface } from './agent-interface'
10
+ import { harnessSettings } from '../settings'
10
11
  import { MockAgentImpl } from './mock-agent'
11
12
  import { OmpRpcAgentImpl } from './omp-agent'
12
13
  import { ClaudeSdkAgentImpl } from './claude-agent'
@@ -36,6 +37,16 @@ export interface HarnessDef {
36
37
  description: string
37
38
  capabilities: HarnessCapabilities
38
39
  create(config: Record<string, unknown>, agent: AgentInfo): AgentInterface
40
+ /** 可用性探测面(harness-availability.ts 消费;缺省 = 进程内引擎,恒可用) */
41
+ probe?: HarnessProbe
42
+ }
43
+
44
+ /** 引擎环境探测声明:进程内引擎无外部依赖;进程型引擎声明将拉起的命令(与真实 spawn 同源) */
45
+ export interface HarnessProbe {
46
+ /** 进程内引擎(mock/claude SDK 骨架):无外部 CLI,恒可用 */
47
+ inprocess?: boolean
48
+ /** 解析将拉起的可执行命令(实例 config.command 覆盖 → 运行时设置缺省 → 内置默认) */
49
+ command?: (config?: Record<string, unknown>) => string
39
50
  }
40
51
 
41
52
  const mockCaps: HarnessCapabilities = {
@@ -57,12 +68,17 @@ const claudeCaps: HarnessCapabilities = {
57
68
  steer: false, supervise: false, hitl: false, terminal: false, contextStats: false, compact: false,
58
69
  }
59
70
 
71
+ /** 实例 config.command 覆盖提取(与各 impl 的 `config.command ?? 缺省` 同规则;空串回缺省) */
72
+ const cmdFrom = (config: Record<string, unknown> | undefined): string =>
73
+ typeof config?.command === 'string' && config.command.trim() !== '' ? config.command.trim() : ''
74
+
60
75
  export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
61
76
  mock: {
62
77
  id: 'mock',
63
78
  label: 'mock(测试)',
64
79
  description: '进程内模拟引擎:联调/测试,无 LLM 调用',
65
80
  capabilities: mockCaps,
81
+ probe: { inprocess: true },
66
82
  create: (config, agent) => new MockAgentImpl({ ...config, agentId: agent.id, name: agent.name, role: agent.role, channelId: agent.channelId, token: agent.token }),
67
83
  },
68
84
  omp: {
@@ -70,6 +86,7 @@ export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
70
86
  label: 'omp(真实 LLM)',
71
87
  description: 'omp 子进程(RPC 模式),默认推荐引擎',
72
88
  capabilities: ompCaps,
89
+ probe: { command: c => cmdFrom(c) || 'omp' },
73
90
  create: (config, agent) => new OmpRpcAgentImpl({ ...config, agentId: agent.id, name: agent.name, role: agent.role, channelId: agent.channelId, token: agent.token }),
74
91
  },
75
92
  opencode: {
@@ -77,6 +94,7 @@ export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
77
94
  label: 'opencode',
78
95
  description: 'OpenCode 引擎(serve 进程 + HTTP/SSE),权限审批走 HITL',
79
96
  capabilities: opencodeCaps,
97
+ probe: { command: c => cmdFrom(c) || harnessSettings().opencode_command },
80
98
  create: (config, agent) => new OpenCodeAgentImpl({ ...config, agentId: agent.id, name: agent.name, role: agent.role, channelId: agent.channelId, token: agent.token }),
81
99
  },
82
100
  codex: {
@@ -84,6 +102,7 @@ export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
84
102
  label: 'codex',
85
103
  description: 'OpenAI Codex CLI(app-server JSON-RPC),命令审批走 HITL',
86
104
  capabilities: codexCaps,
105
+ probe: { command: c => cmdFrom(c) || harnessSettings().codex_command },
87
106
  create: (config, agent) => new CodexAgentImpl({ ...config, agentId: agent.id, name: agent.name, role: agent.role, channelId: agent.channelId, token: agent.token }),
88
107
  },
89
108
  dsh: {
@@ -91,6 +110,7 @@ export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
91
110
  label: 'dsh(DeepSeek)',
92
111
  description: 'DeepSeek Harness(ACP 协议);无同轮 steer,审批走 HITL',
93
112
  capabilities: dshCaps,
113
+ probe: { command: c => cmdFrom(c) || harnessSettings().dsh_command },
94
114
  create: (config, agent) => new DshAgentImpl({ ...config, agentId: agent.id, name: agent.name, role: agent.role, channelId: agent.channelId, token: agent.token }),
95
115
  },
96
116
  claude: {
@@ -98,6 +118,7 @@ export const HARNESS_REGISTRY: Record<string, HarnessDef> = {
98
118
  label: 'claude',
99
119
  description: 'Claude Agent SDK(骨架,待接入)',
100
120
  capabilities: claudeCaps,
121
+ probe: { inprocess: true },
101
122
  create: config => new ClaudeSdkAgentImpl(config),
102
123
  },
103
124
  }
@@ -43,6 +43,7 @@ import { createEnvEmbeddingProvider } from './embedding-provider'
43
43
  import { listHarnessProcesses, listAliveHarnessProcessesByAgent, sweepHarnessProcesses, killHarnessProcess } from '../agents/harness-process'
44
44
  import { hasTerminalSession, sweepTerminalSessions } from '../agents/harness-terminal'
45
45
  import { knownHarnesses } from '../agents/registry'
46
+ import { assertHarnessUsable } from '../agents/harness-availability'
46
47
  import { hostToolsForRole } from '../agents/host-tool-bridge'
47
48
  import { TEAM_AGENT_ID, type MemoryRepo } from '../db/memory.repo'
48
49
  import type { UserRepo } from '../db/user.repo'
@@ -1291,6 +1292,8 @@ export class AgentChannelManager {
1291
1292
  visibility?: string
1292
1293
  ownerUserId?: string | null
1293
1294
  }): Promise<AgentTemplateDetail> {
1295
+ // 执行前强校验:引擎未安装的模板不允许落库(前端下拉已禁用未安装项,此处兜底)
1296
+ assertHarnessUsable(input.harness, input.config)
1294
1297
  const row = this.deps.repos.agents.create({ name: input.name, harness: input.harness, config: input.config, visibility: input.visibility, ownerUserId: input.ownerUserId ?? null })
1295
1298
  return this.templateDetailOf(row)
1296
1299
  }
@@ -1324,6 +1327,7 @@ export class AgentChannelManager {
1324
1327
  async updateAgent(agentId: string, patch: { name?: string, harness?: string, config?: Record<string, unknown>, enabled?: number, visibility?: string }): Promise<AgentTemplateDetail> {
1325
1328
  const row = this.deps.repos.agents.findById(agentId)
1326
1329
  if (!row) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${agentId}`)
1330
+ if (patch.harness) assertHarnessUsable(patch.harness, patch.config ?? parseJson<Record<string, unknown>>(row.configJson, {}))
1327
1331
  const updated = this.deps.repos.agents.update(agentId, patch)
1328
1332
  if (!updated) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${agentId}`)
1329
1333
  return this.templateDetailOf(updated)
@@ -1367,6 +1371,9 @@ export class AgentChannelManager {
1367
1371
  }): Promise<AgentInfo> {
1368
1372
  const tpl = this.deps.repos.agents.findById(input.agentId)
1369
1373
  if (!tpl) throw new AppError(404, 'NOT_FOUND', `Agent 模板不存在: ${input.agentId}`)
1374
+ const instConfig = { ...parseJson<Record<string, unknown>>(tpl.configJson, {}), ...input.configOverride }
1375
+ // 绑定即执行入口:引擎未安装的模板不许入 channel(deploy/加成员在此失败,不产生半成品实例)
1376
+ assertHarnessUsable(tpl.harness, instConfig)
1370
1377
  if (input.role === 'lead') {
1371
1378
  const channel = this.deps.repos.channels.findById(input.channelId)
1372
1379
  if (channel && channel.leadAgentId) {
@@ -1378,7 +1385,7 @@ export class AgentChannelManager {
1378
1385
  templateId: tpl.id,
1379
1386
  name: tpl.name,
1380
1387
  harness: tpl.harness,
1381
- config: { ...parseJson<Record<string, unknown>>(tpl.configJson, {}), ...input.configOverride },
1388
+ config: instConfig,
1382
1389
  role: input.role,
1383
1390
  })
1384
1391
  if (input.role === 'lead') {
@@ -1560,6 +1567,7 @@ export class AgentChannelManager {
1560
1567
  ): Promise<AgentInfo> {
1561
1568
  const m = this.deps.repos.channelAgents.findById(instanceId)
1562
1569
  if (!m) throw new AppError(404, 'NOT_FOUND', `实例不存在: ${instanceId}`)
1570
+ if (patch.harness) assertHarnessUsable(patch.harness, patch.config ?? parseJson<Record<string, unknown>>(m.configJson, {}))
1563
1571
  const updated = this.deps.repos.channelAgents.update(instanceId, patch)
1564
1572
  if (!updated) throw new AppError(404, 'NOT_FOUND', `实例不存在: ${instanceId}`)
1565
1573
  await this.unloadAgent(updated.channelId, instanceId)
@@ -1624,6 +1632,8 @@ export class AgentChannelManager {
1624
1632
  if (!KNOWN_HARNESSES.has(harness)) {
1625
1633
  throw new AppError(400, 'UNKNOWN_HARNESS', `未知 harness: ${harness}(可选 ${[...KNOWN_HARNESSES].join('/')})`)
1626
1634
  }
1635
+ // 先校验引擎可用再落模板,防断言失败遗留孤儿模板行
1636
+ assertHarnessUsable(harness, input.config)
1627
1637
  const tpl = this.deps.repos.agents.create({
1628
1638
  name,
1629
1639
  harness,
@@ -2056,6 +2066,9 @@ export class AgentChannelManager {
2056
2066
  if (input.assigneeId && input.assigneeId !== channel.leadAgentId) {
2057
2067
  assigneeId = this.resolveMemberRef(input.channelId, input.assigneeId).id
2058
2068
  }
2069
+ // 任务执行前强校验:收件引擎不可用直接 409(不留「已创建即失败」的幽灵任务)
2070
+ const assigneeRow = this.deps.repos.channelAgents.findByChannelAgent(input.channelId, assigneeId)
2071
+ if (assigneeRow) assertHarnessUsable(assigneeRow.harness, parseJson<Record<string, unknown>>(assigneeRow.configJson, {}))
2059
2072
  const description = input.mode
2060
2073
  ? encodeTaskMode(input.mode, input.modeConfig ?? {}, input.description ?? '')
2061
2074
  : input.description
@@ -2109,6 +2122,8 @@ export class AgentChannelManager {
2109
2122
  if (!assignee) {
2110
2123
  throw new AppError(403, 'SCOPE_VIOLATION', 'assignee 不在本 channel')
2111
2124
  }
2125
+ // 执行前强校验:worker 引擎未安装 → 报错回 lead(可改派),不产生必败子任务
2126
+ assertHarnessUsable(assignee.harness, parseJson<Record<string, unknown>>(assignee.configJson, {}))
2112
2127
  let task: WorkspaceTask
2113
2128
  if (input.parentTaskId) {
2114
2129
  const parent = this.getTaskEngine().get(input.parentTaskId)
@@ -1 +0,0 @@
1
- import{K as e}from"./rqZ-1SoP.js";import{C as t,H as n,Ht as r,St as i,Wt as a,a as o,b as s,et as c,f as l,g as u,h as d,lt as f,m as p,mt as m,nt as h,p as g,t as _,x as v}from"./BSCpFtzG.js";import{d as ee}from"./-6OnbLFb.js";import{n as y}from"./B5S_UyaC.js";import{n as b}from"./BeIfu4N3.js";import{t as te}from"./VLI2Zy4t.js";import{t as ne}from"./DNxVTfhe.js";import{r as x}from"./D-Ca4BGY.js";import{t as re}from"./RzXYI9FM.js";import{t as S}from"./B-xskWta.js";import{t as C}from"./BBwMH_hG.js";import{t as w}from"./DTYBfjRY.js";import{t as T}from"./Cor9siSf.js";import{t as ie}from"./ORt9oDIy2.js";import{t as ae}from"./C9f8QMp12.js";import{t as E}from"./BcrN7QEl2.js";import{t as D}from"./G0qIw7Gq2.js";import{t as O}from"./CNs_Ozdc.js";var oe={class:`page`},se={key:0,class:`auth-gate`},ce={class:`sub`},le={class:`head`},ue={class:`sub`},de={class:`font-medium`},fe={key:1,class:`tok-cell`},pe=[`title`],me={class:`once-banner`},he={class:`raw-row`},ge={class:`raw`},_e=[`title`],ve=[`title`],ye={class:`once-meta`},k=_(t({__name:`tokens`,setup(t){let{t:_}=b();O({layout:`default`});let k=E(),A=m([]),j=m(!1),M=m(!1),N=m(``),P=m(!1),F=m(``),I=m(``),L=m(!1),R=m(``),z=m(``),B=m(!1),V=async()=>{if(k.isLoggedIn){j.value=!0;try{A.value=await k.listTokens()}catch(e){x.error(e instanceof Error?e.message:_(`tokens.k19jvk54013`))}finally{j.value=!1}}};c(()=>k.isLoggedIn,e=>{e?V():A.value=[]},{immediate:!0});let H=async()=>{P.value=!0;try{let e=await k.createToken(N.value);F.value=e.token,I.value=N.value,M.value=!1,N.value=``,await V()}catch(e){x.error(D(e))}finally{P.value=!1}},U=async()=>{if(!z.value.trim()){x.warning(_(`tokens.k169z26g014`));return}B.value=!0;try{await k.renameToken(R.value,z.value),L.value=!1,await V(),x.success(_(`tokens.k3n9aij015`))}catch(e){x.error(D(e))}finally{B.value=!1}},be=e=>{let t=e.id;if(!t)return;let n=t===k.user?.tokenId;(async()=>{try{if(await k.revokeToken(t),x.success(_(`tokens.k3n64kh016`)),n)return;await V()}catch(e){x.error(D(e))}})()},W=m(!1),G=m(!1),xe=l(()=>{let e=F.value;return e?`${e.slice(0,6)}${`•`.repeat(Math.max(12,e.length-10))}${e.slice(-4)}`:``}),Se=()=>{W.value=!W.value},K=async e=>{try{return await navigator.clipboard.writeText(e),!0}catch{try{let t=document.createElement(`textarea`);t.value=e,t.style.position=`fixed`,t.style.opacity=`0`,document.body.appendChild(t),t.select();let n=document.execCommand(`copy`);return document.body.removeChild(t),n}catch{return!1}}},Ce=async()=>{await K(F.value)?(G.value=!0,setTimeout(()=>{G.value=!1},1600)):x.error(_(`tokens.kjtcn2h017`))},q=()=>{F.value=``,W.value=!1,G.value=!1},J=m({}),Y=m(``),X=m(``),Z=e=>e in J.value,we=e=>Z(e.id)?J.value[e.id]:e.preview??`ut-${`•`.repeat(14)}`,Q=async e=>{if(Z(e.id)){Reflect.deleteProperty(J.value,e.id);return}if(!e.hasPlain){x.warning(_(`tokens.kkpgzo8018`));return}Y.value=e.id;try{let t=await k.revealToken(e.id);t&&(J.value[e.id]=t)}catch(e){x.error(e instanceof Error?e.message:_(`tokens.k1gj9ls019`))}finally{Y.value=``}},Te=async e=>{let t=J.value[e.id]??null;if(!t){if(!e.hasPlain){x.warning(_(`tokens.kr5ovor020`));return}Y.value=e.id;try{t=await k.revealToken(e.id)}catch(e){x.error(e instanceof Error?e.message:_(`tokens.kubtby5021`));return}finally{Y.value=``}}t&&await K(t)?(X.value=e.id,setTimeout(()=>{X.value=``},1600)):x.error(_(`tokens.kgkfcr0022`))},$=e=>e?e.replace(`T`,` `).slice(0,19):`-`,Ee=e=>{R.value=e.id??``,z.value=e.label??``,L.value=!0},De=[{title:_(`tokens.k41416002`),key:`label`,dataIndex:`label`},{title:`Token`,key:`token`,width:360},{title:_(`tokens.k1bg95gk023`),key:`createdAt`,dataIndex:`createdAt`,width:170},{title:_(`tokens.k1euotul024`),key:`lastUsedAt`,dataIndex:`lastUsedAt`,width:200},{title:_(`tokens.k40aa6025`),key:`action`,width:170}];return y({title:()=>_(`titles.tokens`)}),(t,c)=>{let l=te,m=T,_=S,y=ie,b=w,x=ne,E=ae,D=re,O=C;return n(),u(`div`,oe,[i(k).isLoggedIn?(n(),u(o,{key:1},[g(`div`,le,[g(`div`,null,[c[6]||=g(`p`,{class:`aw-kicker`},`agentworkshop / api tokens`,-1),c[7]||=g(`h2`,null,`API Token`,-1),g(`p`,ue,a(i(k).user?.name)+` · `+a(t.$t(`tokens.k1upppaw026`)),1)]),v(y,null,{default:h(()=>[i(k).user?.tokenId?(n(),p(_,{key:0,color:`green`},{default:h(()=>[s(a(t.$t(`tokens.khcxmsc006`)),1)]),_:1})):d(``,!0),v(l,{type:`primary`,onClick:c[1]||=e=>M.value=!0},{default:h(()=>[c[8]||=g(`span`,{class:`i-tabler-plus`},null,-1),s(` `+a(t.$t(`chips.issue`)),1)]),_:1})]),_:1})]),v(E,{spinning:i(j)},{default:h(()=>[v(x,{columns:De,"data-source":i(A),pagination:!1,"row-key":`id`},{bodyCell:h(({column:e,record:f})=>[e.key===`label`?(n(),p(y,{key:0},{default:h(()=>[c[9]||=g(`span`,{class:`i-tabler-key text-primary`},null,-1),g(`span`,de,a(f.label||t.$t(`tokens.kj3mklm028`)),1),f.id===i(k).user?.tokenId?(n(),p(_,{key:0,color:`green`},{default:h(()=>[s(a(t.$t(`tokens.k1defr98007`)),1)]),_:1})):d(``,!0)]),_:2},1024)):e.key===`token`?(n(),u(`div`,fe,[g(`code`,{class:r([`tok-val`,{revealed:Z(f.id)}]),title:Z(f.id)?`已显示明文,点击眼睛遮回`:`掩码预览`},a(we(f)),11,pe),f.hasPlain?(n(),p(l,{key:0,type:`text`,size:`small`,class:`tok-op`,loading:i(Y)===f.id,title:Z(f.id)?`遮回`:`查看明文`,onClick:e=>Q(f)},{default:h(()=>[g(`span`,{class:r(Z(f.id)?`i-tabler-eye-off`:`i-tabler-eye`)},null,2)]),_:2},1032,[`loading`,`title`,`onClick`])):d(``,!0),f.hasPlain?(n(),p(l,{key:1,type:`text`,size:`small`,class:r([`tok-op`,{ok:i(X)===f.id}]),title:i(X)===f.id?`已复制`:`复制明文`,onClick:e=>Te(f)},{default:h(()=>[g(`span`,{class:r(i(X)===f.id?`i-tabler-check`:`i-tabler-copy`)},null,2)]),_:2},1032,[`class`,`title`,`onClick`])):d(``,!0)])):e.key===`createdAt`?(n(),u(o,{key:2},[s(a($(f.createdAt)),1)],64)):e.key===`lastUsedAt`?(n(),u(o,{key:3},[s(a($(f.lastUsedAt)),1)],64)):e.key===`action`?(n(),p(y,{key:4},{default:h(()=>[v(l,{type:`link`,size:`small`,onClick:e=>Ee(f)},{default:h(()=>[c[10]||=g(`span`,{class:`i-tabler-edit`},null,-1),s(` `+a(t.$t(`tokens.k3vrpcs009`)),1)]),_:1},8,[`onClick`]),v(b,{title:f.id===i(k).user?.tokenId?`吊销当前会话 token 将退出登录`:`吊销后立即失效`,"ok-text":`吊销`,"cancel-text":`取消`,onConfirm:e=>be(f)},{default:h(()=>[v(l,{type:`text`,size:`small`,class:`tok-revoke`},{default:h(()=>[c[11]||=g(`span`,{class:`i-tabler-trash`},null,-1),s(` `+a(t.$t(`tokens.k3xmrz010`)),1)]),_:1})]),_:1},8,[`title`,`onConfirm`])]),_:2},1024)):d(``,!0)]),_:1},8,[`data-source`])]),_:1},8,[`spinning`]),v(O,{open:i(M),"onUpdate:open":c[3]||=e=>f(M)?M.value=e:null,title:t.$t(`chips.issueTitle`),"confirm-loading":i(P),"ok-text":`创建`,"cancel-text":`取消`,onOk:H},{default:h(()=>[v(D,{value:i(N),"onUpdate:value":c[2]||=e=>f(N)?N.value=e:null,placeholder:t.$t(`tokens.k1hlqknl001`),onKeydown:e(H,[`enter`])},null,8,[`value`,`placeholder`])]),_:1},8,[`open`,`title`,`confirm-loading`]),v(O,{open:i(F)!==``,title:`Token 已创建`,footer:null,"mask-closable":!1,onCancel:q,onAfterClose:q},{default:h(()=>[g(`div`,me,[c[12]||=g(`span`,{class:`i-tabler-circle-check`},null,-1),g(`span`,null,a(t.$t(`tokens.k12149oy011`)),1)]),g(`div`,he,[g(`code`,ge,a(i(W)?i(F):i(xe)),1),g(`button`,{class:`raw-op`,title:i(W)?`隐藏明文`:`显示明文`,onClick:Se},[g(`span`,{class:r(i(W)?`i-tabler-eye-off`:`i-tabler-eye`)},null,2)],8,_e),g(`button`,{class:r([`raw-op`,{ok:i(G)}]),title:i(G)?`已复制`:`复制`,onClick:Ce},[g(`span`,{class:r(i(G)?`i-tabler-check`:`i-tabler-copy`)},null,2)],10,ve)]),g(`div`,ye,[g(`span`,null,a(t.$t(`tokens.k3p0p44027`))+a(i(I)||t.$t(`tokens.kj3mklm028`)),1),c[13]||=g(`span`,null,`用法:Authorization: Bearer <token>`,-1)]),v(l,{type:`primary`,block:``,onClick:q},{default:h(()=>[s(a(t.$t(`tokens.k1s5f5zd012`)),1)]),_:1})]),_:1},8,[`open`]),v(O,{open:i(L),"onUpdate:open":c[5]||=e=>f(L)?L.value=e:null,title:`重命名 Token`,"confirm-loading":i(B),"ok-text":`保存`,"cancel-text":`取消`,onOk:U},{default:h(()=>[v(D,{value:i(z),"onUpdate:value":c[4]||=e=>f(z)?z.value=e:null,placeholder:t.$t(`tokens.k41416002`),onKeydown:e(U,[`enter`])},null,8,[`value`,`placeholder`])]),_:1},8,[`open`,`confirm-loading`])],64)):(n(),u(`div`,se,[v(m,{class:`auth-card`},{default:h(()=>[g(`h2`,null,a(t.$t(`tokens.k1k6z0r8003`)),1),g(`p`,ce,a(t.$t(`tokens.k1gjnree004`)),1),v(l,{type:`primary`,block:``,onClick:c[0]||=e=>(`navigateTo`in t?t.navigateTo:i(ee))(`/workshop`)},{default:h(()=>[s(a(t.$t(`tokens.k1bhhheq005`)),1)]),_:1})]),_:1})]))])}}}),[[`__scopeId`,`data-v-473f21d7`]]);export{k as default};